第 3 章:对象
原文:
1 语法
对象可以通过两种形式定义:声明(文字)形式和构造形式
声明(文字)形式:
var myObj = { key: value // ...};复制代码
构造形式:
var myObj = new Object();myObj.key = value;复制代码
2 类型
这里书上说 JavaScript 有六种主要类型,ES6 引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
关于 js 的类型 https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive
null
本身是基本类型:null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"
原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。
2.1 内置对象
JavaScript 中还有一些对象子类型,通常被称为内置对象
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数 (由 new 产生的函数调用)来使用,从而可以构造一个对应子类型的新对象
var strPrimitive = "I am a string";typeof strPrimitive; // "string"strPrimitive instanceof String; // falsevar strObject = new String("I am a string");typeof strObject; // "object"strObject instanceof String; // true// 检查 sub-type 对象Object.prototype.toString.call(strObject); // [object String]复制代码
原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。 如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。在必要时语言会自动把字符串字面量转换成一个 String 对象。
Object.prototype.toString…
的用法有个小技巧:https://gist.github.com/Yunkou/67d5da9d05b922479d771d8bcde3308d 判断 js 类型核心的代码:
Object.prototype.toString.call(obj).slice(8, -1);复制代码
基本类型值 "I am a string"
不是一个对象,它是一个不可变的基本字面值。为了对它进行操作,比如检查它的长度,访问它的各个独立字符内容等等,都需要一个 String
对象。
幸运的是,在必要的时候语言会自动地将 "string"
基本类型强制转换为 String
对象类型,这意味着你几乎从不需要明确地创建对象。JS 社区的绝大部分人都 强烈推荐 尽可能地使用字面形式的值,而非使用构造的对象形式。
考虑下面的代码:
var strPrimitive = "I am a string";console.log(strPrimitive.length); // 13console.log(strPrimitive.charAt(3)); // "m"复制代码
在这两个例子中,我们在字符串的基本类型上调用属性和方法,引擎会自动地将它强制转换为 String
对象,所以这些属性/方法的访问可以工作。
null
和 undefined
没有对象包装的形式,仅有它们的基本类型值。相比之下,Date
的值 仅可以 由它们的构造对象形式创建,因为它们没有对应的字面形式。
3 内容
3.1 属性访问
我们需要使用 . 或 [ ] 操作符。
两种语法的主要区别在于,.
操作符后面需要一个 标识符(Identifier)
兼容的属性名,而 [".."]
语法基本可以接收任何兼容 UTF-8/unicode 的字符串作为属性名。举个例子,为了引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"]
语法访问,因为 Super-Fun!
不是一个合法的 Identifier
属性名。 [".."]
语法可以传变量。
在对象中,属性名 总是 字符串。如果你使用 string
以外的(基本)类型值,它会首先被转换为字符串。这甚至包括在数组中常用于索引的数字,所以要小心不要将对象和数组使用的数字搞混了。
var myObject = {};myObject[true] = "foo";myObject[3] = "bar";myObject[myObject] = "baz";myObject["true"]; // "foo"myObject["3"]; // "bar"myObject["[object Object]"]; // "baz"复制代码
如果你试图在一个数组上添加属性,但是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):
var myArray = ["foo", 42, "bar"];myArray["3"] = "baz";myArray.length; // 4myArray[3]; // "baz"复制代码
3.2 属性描述符(Property Descriptors)
在 ES5 之前,JavaScript 语言没有给出直接的方法,让你的代码可以考察或描述属性性质间的区别,比如属性是否为只读。
在 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(..)
来添加新属性,或使用期望的性质来修改既存的属性(如果它是 configurable
的!)。
3.2.1 可写性(Writable)
writable
控制着你改变属性值的能力。
考虑这段代码:
var myObject = {};Object.defineProperty(myObject, "a", { value: 2, writable: false, // 不可写! configurable: true, enumerable: true});myObject.a = 3;myObject.a; // 2复制代码
如你所见,我们对 value
的修改悄无声息地失败了。如果我们在 strict mode
下进行尝试,会得到一个错误:
"use strict";var myObject = {};Object.defineProperty(myObject, "a", { value: 2, writable: false, // 不可写! configurable: true, enumerable: true});myObject.a = 3; // TypeError复制代码
这个 TypeError
告诉我们,我们不能改变一个不可写属性。
注意: 我们一会儿就会讨论 getters/setters,但是简单地说,你可以观察到 writable:false
意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时需要扔出一个 TypeError
,来和 writable:false
保持一致。
3.2.2 可配置性(Configurable)
只要属性当前是可配置的,我们就可以使用相同的 defineProperty(..)
工具,修改它的描述符定义。
var myObject = { a: 2};myObject.a = 3;myObject.a; // 3Object.defineProperty(myObject, "a", { value: 4, writable: true, configurable: false, // 不可配置! enumerable: true});myObject.a; // 4myObject.a = 5;myObject.a; // 5Object.defineProperty(myObject, "a", { value: 6, writable: true, configurable: true, enumerable: true}); // TypeError复制代码
最后的 defineProperty(..)
调用导致了一个 TypeError,这与 strict mode
无关,如果你试图改变一个不可配置属性的描述符定义,就会发生 TypeError。要小心:如你所看到的,将 configurable
设置为 false
是 一个单向操作,不可撤销!
注意: 这里有一个需要注意的微小例外:即便属性已经是 configurable:false
,writable
总是可以没有错误地从 true
改变为 false
,但如果已经是 false
的话不能变回 true
。
configurable:false
阻止的另外一个事情是使用 delete
操作符移除既存属性的能力。
var myObject = { a: 2};myObject.a; // 2delete myObject.a;myObject.a; // undefinedObject.defineProperty(myObject, "a", { value: 2, writable: true, configurable: false, enumerable: true});myObject.a; // 2delete myObject.a;myObject.a; // 2复制代码
如你所见,最后的 delete
调用(无声地)失败了,因为我们将 a
属性设置成了不可配置。
delete
仅用于直接从目标对象移除该对象的(可以被移除的)属性。如果一个对象的属性是某个其他对象/函数的最后一个现存的引用,而你 delete
了它,那么这就移除了这个引用,于是现在那个没有被任何地方所引用的对象/函数就可以被作为垃圾回收。但是,将 delete
当做一个像其他语言(如 C/C++)中那样的释放内存工具是 不 恰当的。delete
仅仅是一个对象属性移除操作 —— 没有更多别的含义。
3.2.3 可枚举性(Enumerable)
我们将要在这里提到的最后一个描述符性质是 enumerable
(还有另外两个,我们将在一会儿讨论 getter/setters 时谈到)。
它的名称可能已经使它的功能很明显了,这个性质控制着一个属性是否能在特定的对象-属性枚举操作中出现,比如 for..in
循环。设置为 false
将会阻止它出现在这样的枚举中,即使它依然完全是可以访问的。设置为 true
会使它出现。
所有普通的用户定义属性都默认是可 enumerable
的,正如你通常希望的那样。但如果你有一个特殊的属性,你想让它对枚举隐藏,就将它设置为 enumerable:false
。
3.3 存在性(Existence)
我们可以查询一个对象是否拥有特定的属性,而 不必 取得那个属性的值:
var myObject = { a: 2};"a" in myObject; // true"b" in myObject; // falsemyObject.hasOwnProperty("a"); // truemyObject.hasOwnProperty("b"); // false复制代码
in
操作符会检查属性是否存在于对象 中,或者是否存在于 [[Prototype]]
链对象遍历的更高层中(详见第五章)。相比之下,hasOwnProperty(..)
仅仅 检查 myObject
是否拥有属性,但 不会 查询 [[Prototype]]
链。我们会在第五章详细讲解 [[Prototype]]
时,回来讨论这个两个操作重要的不同。
通过委托到 Object.prototype
,所有的普通对象都可以访问 hasOwnProperty(..)
(详见第五章)。但是创建一个不链接到 Object.prototype
的对象也是可能的(通过 Object.create(null)
—— 详见第五章)。这种情况下,像 myObject.hasOwnProperty(..)
这样的方法调用将会失败。
在这种场景下,一个进行这种检查的更健壮的方式是 Object.prototype.hasOwnProperty.call(myObject,"a")
,它借用基本的 hasOwnProperty(..)
方法而且使用 明确的 this 绑定(详见第二章)来对我们的 myObject
实施这个方法。
注意: in
操作符看起来像是要检查一个值在容器中的存在性,但是它实际上检查的是属性名的存在性。在使用数组时注意这个区别十分重要,因为我们会有很强的冲动来进行 4 in [2, 4, 6]
这样的检查,但是这总是不像我们想象的那样工作。
3.3.1 枚举(Enumeration)
先前,在学习 enumerable
属性描述符性质时,我们简单地解释了"可枚举性(enumerability)"的含义。现在,让我们来更加详细地重新讲解它。
var myObject = {};Object.defineProperty( myObject, "a", // 使 `a` 可枚举,如一般情况 { enumerable: true, value: 2 });Object.defineProperty( myObject, "b", // 使 `b` 不可枚举 { enumerable: false, value: 3 });myObject.b; // 3"b" in myObject; // truemyObject.hasOwnProperty("b"); // true// .......for (var k in myObject) { console.log(k, myObject[k]);}// "a" 2复制代码
你会注意到,myObject.b
实际上 存在,而且拥有可以访问的值,但是它不出现在 for..in
循环中(然而令人诧异的是,它的 in
操作符的存在性检查通过了)。这是因为 “enumerable” 基本上意味着“如果对象的属性被迭代时会被包含在内”。
注意: 将 for..in
循环实施在数组上可能会给出意外的结果,因为枚举一个数组将不仅包含所有的数字下标,还包含所有的可枚举属性。所以一个好主意是:将 for..in
循环 仅 用于对象,而为存储在数组中的值使用传统的 for
循环并用数字索引迭代。
另一个可以区分可枚举和不可枚举属性的方法是:
var myObject = {};Object.defineProperty( myObject, "a", // 使 `a` 可枚举,如一般情况 { enumerable: true, value: 2 });Object.defineProperty( myObject, "b", // 使 `b` 不可枚举 { enumerable: false, value: 3 });myObject.propertyIsEnumerable("a"); // truemyObject.propertyIsEnumerable("b"); // falseObject.keys(myObject); // ["a"]Object.getOwnPropertyNames(myObject); // ["a", "b"]复制代码
propertyIsEnumerable(..)
测试一个给定的属性名是否直 接存 在于对象上,并且是 enumerable:true
。
Object.keys(..)
返回一个所有可枚举属性的数组,
Object.getOwnPropertyNames(..)
返回一个 所有 属性的数组,不论能不能枚举。
in
和 hasOwnProperty(..)
区别于它们是否查询 [[Prototype]]
链,
Object.keys(..)
和 Object.getOwnPropertyNames(..)
都 只 考察直接给定的对象。
(当下)没有与 in
操作符的查询方式(在整个 [[Prototype]]
链上遍历所有的属性,如我们在第五章解释的)等价的、内建的方法可以得到一个 所有属性 的列表。你可以近似地模拟一个这样的工具:递归地遍历一个对象的 [[Prototype]]
链,在每一层都从 Object.keys(..)
中取得一个列表——仅包含可枚举属性。
3.4 迭代(Iteration)
for..in
循环迭代一个对象上(包括它的 [[Prototype]]
链)所有的可迭代属性。但如果你想要迭代值呢?
在数字索引的数组中,典型的迭代所有的值的办法是使用标准的 for
循环,比如:
var myArray = [1, 2, 3];for (var i = 0; i < myArray.length; i++) { console.log(myArray[i]);}// 1 2 3复制代码
但是这并没有迭代所有的值,而是迭代了所有的下标,然后由你使用索引来引用值,比如 myArray[i]
。
ES5 还为数组加入了几个迭代帮助方法,包括 forEach(..)
、every(..)
、和 some(..)
。这些帮助方法的每一个都接收一个回调函数,这个函数将施用于数组中的每一个元素,仅在如何响应回调的返回值上有所不同。
forEach(..)
将会迭代数组中所有的值,并且忽略回调的返回值。every(..)
会一直迭代到最后,或者 当回调返回一个 false
(或“falsy”)值,而 some(..)
会一直迭代到最后,或者 当回调返回一个 true
(或“truthy”)值。
这些在 every(..)
和 some(..)
内部的特殊返回值有些像普通 for
循环中的 break
语句,它们可以在迭代执行到末尾之前将它结束掉。
如果你使用 for..in
循环在一个对象上进行迭代,你也只能间接地得到值,因为它实际上仅仅迭代对象的所有可枚举属性,让你自己手动地去访问属性来得到值。
注意: 与以有序数字的方式(for
循环或其他迭代器)迭代数组的下标比较起来,迭代对象属性的顺序是 不确定 的,而且可能会因 JS 引擎的不同而不同。对于需要跨平台环境保持一致的问题,不要依赖 观察到的顺序,因为这个顺序是不可靠的。
但是如果你想直接迭代值,而不是数组下标(或对象属性)呢?ES6 加入了一个有用的 for..of
循环语法,用来迭代数组(和对象,如果这个对象有定义的迭代器):
var myArray = [1, 2, 3];for (var v of myArray) { console.log(v);}// 1// 2// 3复制代码
for..of
循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫做 @@iterator
的默认内部函数那里得到),每次循环都调用一次这个迭代器对象的 next()
方法,循环迭代的内容就是这些连续的返回值。
数组拥有内建的 @@iterator
,所以正如展示的那样,for..of
对于它们很容易使用。但是让我们使用内建的 @@iterator
来手动迭代一个数组,来看看它是怎么工作的:
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 }复制代码
注意: 我们使用一个 ES6 的 Symbol
:Symbol.iterator
来取得一个对象的 @@iterator
内部属性。我们在本章中简单地提到过 Symbol
的语义(见“计算型属性名”),同样的原理也适用于这里。你总是希望通过 Symbol
名称,而不是它可能持有的特殊的值,来引用这样特殊的属性。另外,尽管这个名称有这样的暗示,但 @@iterator
本身 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!
正如上面的代码段揭示的,迭代器的 next()
调用的返回值是一个 { value: .. , done: .. }
形式的对象,其中 value
是当前迭代的值,而 done
是一个 boolean
,表示是否还有更多内容可以迭代。
注意值 3
和 done:false
一起返回,猛地一看会有些奇怪。你不得不第四次调用 next()
(在前一个代码段的 for..of
循环会自动这样做)来得到 done:true
,以使自己知道迭代已经完成。这个怪异之处的原因超出了我们要在这里讨论的范围,但是它源自于 ES6 生成器(generator)函数的语义。
虽然数组可以在 for..of
循环中自动迭代,但普通的对象 没有内建的 @@iterator。这种故意省略的原因要比我们将在这里解释的更复杂,但一般来说,为了未来的对象类型,最好不要加入那些可能最终被证明是麻烦的实现。
复习
JS 中的对象拥有字面形式(比如 var a = { .. }
)和构造形式(比如 var a = new Array(..)
)。字面形式几乎总是首选,但在某些情况下,构造形式提供更多的构建选项。
许多人声称“Javascript 中的一切都是对象”,这是不对的。对象是六种(或七中,看你从哪个方面说)基本类型之一。对象有子类型,包括 function
,还可以被行为特化,比如 [object Array]
作为内部的标签表示子类型数组。
对象是键/值对的集合。通过 .propName
或 ["propName"]
语法,值可以作为属性访问。不管属性什么时候被访问,引擎实际上会调用内部默认的 [[Get]]
操作(在设置值时调用 [[Put]]
操作),它不仅直接在对象上查找属性,在没有找到时还会遍历 [[Prototype]]
链(见第五章)。
属性有一些可以通过属性描述符控制的特定性质,比如 writable
和 configurable
。另外,对象拥有它的不可变性(它们的属性也有),可以通过使用 Object.preventExtensions(..)
、Object.seal(..)
、和 Object.freeze(..)
来控制几种不同等级的不可变性。
属性不必非要包含值 —— 它们也可以是带有 getter/setter 的“访问器属性”。它们也可以是可枚举或不可枚举的,这控制它们是否会在 for..in
这样的循环迭代中出现。
你也可以使用 ES6 的 for..of
语法,在数据结构(数组,对象等)中迭代 值,它寻找一个内建或自定义的 @@iterator
对象,这个对象由一个 next()
方法组成,通过这个 next()
方法每次迭代一个数据。