语法
对象来自于两种形式:声明(字面)形式,和构造形式。
var myObj = { --> 字面形式
key: value
// ...
};
var myObj = new Object(); --> 构造形式
myObj.key = value;
类型
JS 的六种主要类型(在语言规范中称为“语言类型”)中的一种:
string
number
boolean
null
undefined
object
简单基本类型 (string
、number
、boolean
、null
、和 undefined
)自身 不是 object
。null
有时会被当成一个对象类型,但是这种误解源自于一个语言中的 Bug,它使得 typeof null
错误地(而且令人困惑地)返回字符串 "object"
。实际上,null
是它自己的基本类型
一个常见的错误论断是“JavaScript中的一切都是对象”。这明显是不对的
**
function
是对象的一种子类型(技术上讲,叫做“可调用对象”)- 数组也是一种形式的对象,带有特别的行为
内建对象
String
Number
Boolean
Object
Function
Array
Date
RegExp
Error
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
JS 社区的绝大部分人都 强烈推荐 尽可能地使用字面形式的值,而非使用构造的对象形式。因为必要时候基本类型会被强制转换为对应的内建对象
内容
对象的内容由存储在特定命名的 位置 上的(任意类型的)值组成,我们称这些值为属性
当我们说“内容”时,似乎暗示着这些值 实际上 存储在对象内部,但那只不过是表面现象。引擎会根据自己的实现来存储这些值,而且通常都不是把它们存储在容器对象 内部。在容器内存储的是这些属性的名称,它们像指针(技术上讲,叫 引用(reference))一样指向值存储的地方
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
为了访问 myObject
在 位置 a
的值,我们需要使用 .
或 [ ]
操作符。.a
语法通常称为“属性(property)”访问,而 ["a"]
语法通常称为“键(key)”访问。都是属于“属性访问”
两种语法的主要区别在于,.
操作符后面需要一个 标识符(Identifier)
兼容的属性名,而 [".."]
语法基本可以接收任何兼容 UTF-8/unicode 的字符串作为属性名。比如引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"]
语法访问,因为 Super-Fun!
不是一个合法的 Identifier
属性名;还有个好处就是程序可以动态地组建字符串的值来作为属性名。
在对象中,属性名 总是 字符串。如果你使用 string
以外的(基本)类型值,它会首先被转换为字符串。这甚至包括在数组中常用于索引的数字,所以要小心不要将对象和数组使用的数字搞混了
var myObject = { };
myObject[true] = "foo"; --> 属性名字为 “true”
myObject[3] = "bar"; --> 属性名为 '3'
myObject[myObject] = "baz"; -->属性名为 '[object Object]'
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
计算型属性名
ES6 加入了 计算型属性名,在一个字面对象声明的键名称位置,你可以指定一个表达式,用 [ ]
括起来
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
计算型属性名 的最常见用法,可能是用于 ES6 的 [Symbol](https://www.yuque.com/tuyong/skill/zcrgia#35850a95)
属性(Property) vs. 方法(Method)
每次你访问一个对象的属性都是一个 属性访问,无论你得到什么类型的值。如果你 恰好 从属性访问中得到一个函数,它也没有魔法般地在那时成为一个“方法”。一个从属性访问得来的函数没有任何特殊性(隐含的 this
绑定处通过最终函数调用点已经解释,this又运行时决定而非编写时)
就算声明一个函数表达式作为字面对象的一部分,那个函数都不会地 属于 这个对象 —— 仍然仅仅是同一个函数对象的多个引用罢了
var myObject = {
foo: function foo() {
console.log( "foo" );
}
};
var someFoo = myObject.foo;
someFoo; // function foo(){..}
myObject.foo; // function foo(){..}
数组
数组也使用 [ ]
访问形式,在存储值的方式和位置上它们的组织更加结构化(虽然仍然在存储值的 类型 上没有限制)。数组采用 数字索引,这意味着值被存储的位置,通常称为 下标,是一个非负整数
但数组也是对象,所以虽然每个索引都是正整数,你还可以在数组上添加属性:
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz"; // --> 对这个数组对象新增加一个属性“bar”
myArray.length; // 3 --> 新增加的属性不会影响数组长度!!!
myArray.baz; // "baz"
如果你试图在一个数组上添加属性,但是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz" --> 上面的3这个被转换成了一个数字索引,因为改变了数组的长度,并且设置了
索引为3的数组的值
复制对象
var myObject = {
a: 2,
b: anotherObject, // 引用,不是拷贝!
c: anotherArray, // 又一个引用!
d: anotherFunction
};
一个 浅拷贝(shallow copy) 会得到一个新对象,它的 a
是值 2
的拷贝,但 b
、c
和 d
属性仅仅是引用,它们指向被拷贝对象中引用的相同位置。一个 深拷贝(deep copy) 将不仅复制 myObject
,还会复制 anotherObject
和 anotherArray
JSON 安全的对象(也就是,可以被序列化为一个 JSON 字符串,之后还可以被重新解析为拥有相同的结构和值的对象)可以简单地这样 复制:
var newObj = JSON.parse( JSON.stringify( someObj ) );
(浅拷贝)ES6 为此任务已经定义了 Object.assign(..)
。Object.assign(..)
接收 目标 对象作为第一个参数,然后是一个或多个 源 对象作为后续参数
var newObj = Object.assign( {}, myObject );
然而在 Object.assign(..)
中发生的复制是单纯的 =
式赋值,所以任何在源对象属性的特殊性质(比如 writable
)在目标对象上 都不会保留 。
属性描述符(Property Descriptors)
在 ES5 中,所有的属性都用 属性描述符(Property Descriptors) 来描述。
var myObject = {
a: 2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
我们普通的对象属性 a
的属性描述符的内容要比 value
为 2
多得多。它还包含另外三个性质:writable
、enumerable
、和 configurable
可以通过如下代码动态定义某个属性的这三个性质
Object.defineProperty( myObject, "a", {
value: 2,
writable: false,
configurable: true,
enumerable: true
} );
Writable 可写性
当值被设置成false时,任意的赋值操作都无效;并且下 strict mode
下会抛出TypeError
错误; writable:false
意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时需要扔出一个 TypeError
,来和 writable:false
保持一致
myObject.a = 3; --> 赋值无效
myObject.a; // 2
可配置性(Configurable)
将 configurable
设置为 false
是 一个单向操作,不可撤销!configurable:false
阻止的另外一个事情是使用 delete
操作符移除既存属性的能力
可枚举性(Enumerable)
这个性质控制着一个属性是否能在特定的对象-属性枚举操作中出现,比如 for..in
循环。设置为 false
将会阻止它出现在这样的枚举中,即使它依然完全是可以访问的。设置为 true
会使它出现
所有普通的用户定义属性都默认是可 enumerable
的
propertyIsEnumerable(..)
测试一个给定的属性名是否直 接存 在于对象上,并且是enumerable:true
。Object.keys(..)
返回一个所有可枚举属性的数组Object.getOwnPropertyNames(..)
返回一个 所有 属性的数组,不论能不能枚举。
不可变性(Immutability)
将属性或对象(有意或无意地)设置为不可改变的
所有 这些方法创建的都是浅不可变性(内部对象引用不受影响)。也就是,它们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组、对象、函数等)的引用,那个对象的 内容 不会受影响,任然保持可变
对象常量(Object Constant)
通过将 writable:false
与 configurable:false
组合,你可以实质上创建了一个作为对象属性的 常量(不能被改变,重定义或删除),比如:
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
防止扩展(Prevent Extensions)
如果你想防止一个对象被添加新的属性,但另一方面保留其他既存的对象属性,可以调用 Object.preventExtensions(..)
:
var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
封印(Seal)
Object.seal(..)
创建一个“封印”的对象,这意味着它实质上在当前的对象上调用 Object.preventExtensions(..)
,同时也将它所有的既存属性标记为 configurable:false
。
所以,你既不能添加更多的属性,也不能重新配置或删除既存属性(虽然你依然 可以 修改它们的值)。
冻结(Freeze)
Object.freeze(..)
创建一个冻结的对象,这意味着它实质上在当前的对象上调用 Object.seal(..)
,同时也将它所有的“数据访问”属性设置为 writable:false
,所以它们的值不可改变
[[Get]]
属性访问
对一个对象进行默认的内建 [[Get]]
操作,会 首先 检查对象,寻找一个拥有被请求的名称的属性,如果找到,就返回相应的值。
然而,如果按照被请求的名称 没能 找到属性,[[Get]]
的算法定义了另一个重要的行为。我们会在第五章来解释 接下来 会发生什么(遍历 [[Prototype]]
链,如果有的话)
通过任何方法都不能找到被请求的属性的值,那么它会返回 undefined
。
[[Put]]
调用 [[Put]]
时,它根据几个因素表现不同的行为,包括(影响最大的)属性是否已经在对象中存在了。
如果属性存在,[[Put]]
算法将会大致检查:
- 这个属性是访问器描述符吗(见下一节”Getters 与 Setters”)?如果是,而且是 setter,就调用 setter。
- 这个属性是
writable
为false
数据(属性)描述符吗?如果是,在非strict mode
下无声地失败,或者在strict mode
下抛出TypeError
。 - 否则,像平常一样设置既存属性的值。
Getters 与 Setters
ES5 引入了一个方法来覆盖这些默认操作的一部分,但不是在对象级别而是针对每个属性,就是通过 getters 和 setters。Getter 是实际上调用一个隐藏函数来取得值的属性。Setter 是实际上调用一个隐藏函数来设置值的属性。
当你将一个属性定义为拥有 getter 或 setter 或两者兼备,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对)。对于访问器描述符,它的 value
和 writable
性质因没有意义而被忽略,取而代之的是 JS 将会考虑属性的 set
和 get
性质(还有 configurable
和 enumerable
)
var myObject = {
// 为 `a` 定义一个 getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 为 `b` 定义 getter
get: function(){ return this.a * 2 }, //this的调用点是在myObject内部,因此可访问 a
// 确保 `b` 作为对象属性出现
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
setter的操作
var myObject = {
// 为 `a` 定义 getter
get a() { --> 这里a 就表示属性a名称,且必须是 属性的名称,要么无法重定义属性的set/get成功
return this._a_; // _a_只是一个代名词而已,可以用任意的名字替换 如 value
},
// 为 `a` 定义 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
存在性(Existence)
我们可以查询一个对象是否拥有特定的属性,而 不必 取得那个属性的值:
var myObject = {
a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in
操作 和 hasOwnProperty
操作的不同
in
操作符会检查属性是否存在于对象 中,或者是否存在于[[Prototype]]
链对象遍历的更高层中hasOwnProperty(..)
仅仅 检查myObject
是否拥有属性,但 不会 查询[[Prototype]]
链
通过委托到 Object.prototype
,所有的普通对象都可以访问 hasOwnProperty(..)
。但是创建一个不链接到 Object.prototype
的对象也是可能的(通过 Object.create(null)
)。这种情况下,像 myObject.hasOwnProperty(..)
这样的方法调用将会失败。
在这种场景下,一个进行这种检查的更健壮的方式是 Object.prototype.hasOwnProperty.call(myObject,"a")
,它借用基本的 hasOwnProperty(..)
方法而且使用 明确的 this
绑定 来对我们的 myObject
实施调用这个方法
迭代(Iteration)
ES5 还为数组加入了几个迭代帮助方法,forEach(..)
将会迭代数组中所有的值,并且忽略回调的返回值。every(..)
会一直迭代到最后,或者 当回调返回一个 false
(或“falsy”)值,而 some(..)
会一直迭代到最后,或者 当回调返回一个 true
(或“truthy”)值。
ES6 加入了一个有用的 for..of
循环语法,用来迭代数组(和对象,如果这个对象有定义的迭代器),不关心下标!
for..of
循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫做 @@iterator
的默认内部函数那里得到),每次循环都调用一次这个迭代器对象的 next()
方法,循环迭代的内容就是这些连续的返回值。
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
通过 Symbol
名称,而不是它可能持有的特殊的值,来引用这样特殊的属性(属性名就是Symbol``.``iterator
)。另外,尽管这个名称有这样的暗示,但 @@iterator
本身 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!
这章与generator有很大的牵扯,需要再次看看