第三章: 对象 - 内容

优质
小牛编辑
122浏览
2023-12-01

内容

正如刚才提到的,对象的内容由存储在特定命名的 位置 上的(任意类型的)值组成,我们称这些值为属性。

有一个重要的事情需要注意:当我们说“内容”时,似乎暗示着这些值 实际上 存储在对象内部,但那只不过是表面现象。引擎会根据自己的实现来存储这些值,而且通常都不是把它们存储在容器对象 内部。在容器内存储的是这些属性的名称,它们像指针(技术上讲,叫 引用(reference))一样指向值存储的地方。

考虑下面的代码:

  1. var myObject = {
  2. a: 2
  3. };
  4. myObject.a; // 2
  5. myObject["a"]; // 2

为了访问 myObject位置 a 的值,我们需要使用 .[ ] 操作符。.a 语法通常称为“属性(property)”访问,而 ["a"] 语法通常称为“键(key)”访问。在现实中,它们俩都访问相同的 位置,而且会拿出相同的值,2,所以这些术语可以互换使用。从现在起,我们将使用最常见的术语 —— “属性访问”。

两种语法的主要区别在于,. 操作符后面需要一个 标识符(Identifier) 兼容的属性名,而 [".."] 语法基本可以接收任何兼容 UTF-8/unicode 的字符串作为属性名。举个例子,为了引用一个名为“Super-Fun!”的属性,你不得不使用 ["Super-Fun!"] 语法访问,因为 Super-Fun! 不是一个合法的 Identifier 属性名。

而且,由于 [".."] 语法使用字符串的 来指定位置,这意味着程序可以动态地组建字符串的值。比如:

  1. var wantA = true;
  2. var myObject = {
  3. a: 2
  4. };
  5. var idx;
  6. if (wantA) {
  7. idx = "a";
  8. }
  9. // 稍后
  10. console.log( myObject[idx] ); // 2

在对象中,属性名 总是 字符串。如果你使用 string 以外的(基本)类型值,它会首先被转换为字符串。这甚至包括在数组中常用于索引的数字,所以要小心不要将对象和数组使用的数字搞混了。

  1. var myObject = { };
  2. myObject[true] = "foo";
  3. myObject[3] = "bar";
  4. myObject[myObject] = "baz";
  5. myObject["true"]; // "foo"
  6. myObject["3"]; // "bar"
  7. myObject["[object Object]"]; // "baz"

计算型属性名

如果你需要将一个计算表达式 作为 一个键名称,那么我们刚刚描述的 myObject[..] 属性访问语法是十分有用的,比如 myObject[prefix + name]。但是当使用字面对象语法声明对象时则没有什么帮助。

ES6 加入了 计算型属性名,在一个字面对象声明的键名称位置,你可以指定一个表达式,用 [ ] 括起来:

  1. var prefix = "foo";
  2. var myObject = {
  3. [prefix + "bar"]: "hello",
  4. [prefix + "baz"]: "world"
  5. };
  6. myObject["foobar"]; // hello
  7. myObject["foobaz"]; // world

计算型属性名 的最常见用法,可能是用于 ES6 的 Symbol,我们将不会在本书中涵盖关于它的细节。简单地说,它们是新的基本数据类型,拥有一个不透明不可知的值(技术上讲是一个 string 值)。你将会被强烈地不鼓励使用一个 Symbol实际值 (这个值理论上会因 JS 引擎的不同而不同),所以 Symbol 的名称,比如 Symbol.Something(这是个瞎编的名称!),才是你会使用的:

  1. var myObject = {
  2. [Symbol.Something]: "hello world"
  3. };

属性(Property) vs. 方法(Method)

有些开发者喜欢在讨论对一个对象的属性访问时做一个区别,如果这个被访问的值恰好是一个函数的话。因为这诱使人们认为函数 属于 这个对象,而且在其他语言中,属于对象(也就是“类”)的函数被称作“方法”,所以相对于“属性访问”,我们常能听到“方法访问”。

有趣的是,语言规范也做出了同样的区别

从技术上讲,函数绝不会“属于”对象,所以,说一个偶然在对象的引用上被访问的函数就自动地成为了一个“方法”,看起来有些像是牵强附会。

有些函数内部确实拥有 this 引用,而且 有时 这些 this 引用指向调用点的对象引用。但这个用法确实没有使这个函数比其他函数更像“方法”,因为 this 是在运行时在调用点动态绑定的,这使得它与这个对象的关系至多是间接的。

每次你访问一个对象的属性都是一个 属性访问,无论你得到什么类型的值。如果你 恰好 从属性访问中得到一个函数,它也没有魔法般地在那时成为一个“方法”。一个从属性访问得来的函数没有任何特殊性(隐含的 this 绑定的情况在刚才已经解释过了)。

举个例子:

  1. function foo() {
  2. console.log( "foo" );
  3. }
  4. var someFoo = foo; // 对 `foo` 的变量引用
  5. var myObject = {
  6. someFoo: foo
  7. };
  8. foo; // function foo(){..}
  9. someFoo; // function foo(){..}
  10. myObject.someFoo; // function foo(){..}

someFoomyObject.someFoo 只不过是同一个函数的两个分离的引用,它们中的任何一个都不意味着这个函数很特别或被其他对象所“拥有”。如果上面的 foo() 定义里面拥有一个 this 引用,那么 myObject.someFoo隐含绑定 将会是这个两个引用间 唯一 可以观察到的不同。它们中的任何一个都没有称为“方法”的道理。

也许有人会争辩,函数 变成了方法,不是在定义期间,而是在调用的执行期间,根据它是如何在调用点被调用的(是否带有一个环境对象引用 —— 细节见第二章)。即便是这种解读也有些牵强。

可能最安全的结论是,在 JavaScript 中,“函数”和“方法”是可以互换使用的。

注意: ES6 加入了 super 引用,它通常是和 class(见附录A)一起使用的。super 的行为方式(静态绑定,而非像 this 一样延迟绑定),给了这种说法更多的权重:一个被 super 绑定到某处的函数比起“函数”更像一个“方法”。但是同样地,这仅仅是微妙的语义上的(和机制上的)细微区别。

就算你声明一个函数表达式作为字面对象的一部分,那个函数都不会魔法般地 属于 这个对象 —— 仍然仅仅是同一个函数对象的多个引用罢了。

  1. var myObject = {
  2. foo: function foo() {
  3. console.log( "foo" );
  4. }
  5. };
  6. var someFoo = myObject.foo;
  7. someFoo; // function foo(){..}
  8. myObject.foo; // function foo(){..}

注意: 在第六章中,我们会为字面对象的 foo: function foo(){ .. } 声明语法介绍一种ES6的简化语法。

数组

数组也使用 [ ] 访问形式,但正如上面提到的,在存储值的方式和位置上它们的组织更加结构化(虽然仍然在存储值的 类型 上没有限制)。数组采用 数字索引,这意味着值被存储的位置,通常称为 下标,是一个非负整数,比如 042

  1. var myArray = [ "foo", 42, "bar" ];
  2. myArray.length; // 3
  3. myArray[0]; // "foo"
  4. myArray[2]; // "bar"

数组也是对象,所以虽然每个索引都是正整数,你还可以在数组上添加属性:

  1. var myArray = [ "foo", 42, "bar" ];
  2. myArray.baz = "baz";
  3. myArray.length; // 3
  4. myArray.baz; // "baz"

注意,添加命名属性(不论是使用 . 还是 [ ] 操作符语法)不会改变数组的 length 所报告的值。

可以 把一个数组当做普通的键/值对象使用,并且从不添加任何数字下标,但这不是一个好主意,因为数组对它本来的用途有着特定的行为和优化方式,普通对象也一样。使用对象来存储键/值对,而用数组在数字下标上存储值。

小心: 如果你试图在一个数组上添加属性,但是属性名 看起来 像一个数字,那么最终它会成为一个数字索引(也就是改变了数组的内容):

  1. var myArray = [ "foo", 42, "bar" ];
  2. myArray["3"] = "baz";
  3. myArray.length; // 4
  4. myArray[3]; // "baz"

复制对象

当开发者们初次拿起 Javascript 语言时,最常需要的特性就是如何复制一个对象。看起来应该有一个内建的 copy() 方法,对吧?但是事情实际上比这复杂一些,因为在默认情况下,复制的算法应当是什么,并不十分明确。

例如,考虑这个对象:

  1. function anotherFunction() { /*..*/ }
  2. var anotherObject = {
  3. c: true
  4. };
  5. var anotherArray = [];
  6. var myObject = {
  7. a: 2,
  8. b: anotherObject, // 引用,不是拷贝!
  9. c: anotherArray, // 又一个引用!
  10. d: anotherFunction
  11. };
  12. anotherArray.push( anotherObject, myObject );

一个myObject拷贝 究竟应该怎么表现?

首先,我们应该回答它是一个 浅(shallow) 还是一个 深(deep) 拷贝?一个 浅拷贝(shallow copy) 会得到一个新对象,它的 a 是值 2 的拷贝,但 bcd 属性仅仅是引用,它们指向被拷贝对象中引用的相同位置。一个 深拷贝(deep copy) 将不仅复制 myObject,还会复制 anotherObjectanotherArray。但之后我们让 anotherArray 拥有 anotherObjectmyObject 的引用,所以 那些 也应当被复制而不是仅保留引用。现在由于循环引用,我们得到了一个无限循环复制的问题。

我们应当检测循环引用并打破循环遍历吗(不管位于深处的,没有完全复制的元素)?我们应当报错退出吗?或者介于两者之间?

另外,“复制”一个函数意味着什么,也不是很清楚。有一些技巧,比如提取一个函数源代码的 toString() 序列化表达(这个源代码会因实现不同而不同,而且根据被考察的函数的类型,其结果甚至在所有引擎上都不可靠)。

那么我们如何解决所有这些刁钻的问题?不同的 JS 框架都各自挑选自己的解释并且做出自己的选择。但是哪一种(如果有的话)才是 JS 应当作为标准采用的呢?长久以来,没有明确答案。

一个解决方案是,JSON 安全的对象(也就是,可以被序列化为一个 JSON 字符串,之后还可以被重新解析为拥有相同的结构和值的对象)可以简单地这样 复制

  1. var newObj = JSON.parse( JSON.stringify( someObj ) );

当然,这要求你保证你的对象是 JSON 安全的。对于某些情况,这没什么大不了的。而对另一些情况,这还不够。

同时,浅拷贝相当易懂,而且没有那么多问题,所以 ES6 为此任务已经定义了 Object.assign(..)Object.assign(..) 接收 目标 对象作为第一个参数,然后是一个或多个 对象作为后续参数。它会在 对象上迭代所有的 可枚举(enumerable)owned keys直接拥有的键),并把它们拷贝到 目标 对象上(仅通过 = 赋值)。它还会很方便地返回 目标 对象,正如下面你可以看到的:

  1. var newObj = Object.assign( {}, myObject );
  2. newObj.a; // 2
  3. newObj.b === anotherObject; // true
  4. newObj.c === anotherArray; // true
  5. newObj.d === anotherFunction; // true

注意: 在下一部分中,我们将讨论“属性描述符(property descriptors —— 属性的性质)”并展示 Object.defineProperty(..) 的使用。然而在 Object.assign(..) 中发生的复制是单纯的 = 式赋值,所以任何在源对象属性的特殊性质(比如 writable)在目标对象上 都不会保留

属性描述符(Property Descriptors)

在 ES5 之前,JavaScript 语言没有给出直接的方法,让你的代码可以考察或描述属性性质间的区别,比如属性是否为只读。

在 ES5 中,所有的属性都用 属性描述符(Property Descriptors) 来描述。

考虑这段代码:

  1. var myObject = {
  2. a: 2
  3. };
  4. Object.getOwnPropertyDescriptor( myObject, "a" );
  5. // {
  6. // value: 2,
  7. // writable: true,
  8. // enumerable: true,
  9. // configurable: true
  10. // }

正如你所见,我们普通的对象属性 a 的属性描述符(称为“数据描述符”,因为它仅持有一个数据值)的内容要比 value2 多得多。它还包含另外三个性质:writableenumerable、和 configurable

当我们创建一个普通属性时,可以看到属性描述符的各种性质的默认值,同时我们可以用 Object.defineProperty(..) 来添加新属性,或使用期望的性质来修改既存的属性(如果它是 configurable 的!)。

举例来说:

  1. var myObject = {};
  2. Object.defineProperty( myObject, "a", {
  3. value: 2,
  4. writable: true,
  5. configurable: true,
  6. enumerable: true
  7. } );
  8. myObject.a; // 2

使用 defineProperty(..),我们手动、明确地在 myObject 上添加了一个直白的,普通的 a 属性。然而,你通常不会使用这种手动方法,除非你想要把描述符的某个性质修改为不同的值。

可写性(Writable)

writable 控制着你改变属性值的能力。

考虑这段代码:

  1. var myObject = {};
  2. Object.defineProperty( myObject, "a", {
  3. value: 2,
  4. writable: false, // 不可写!
  5. configurable: true,
  6. enumerable: true
  7. } );
  8. myObject.a = 3;
  9. myObject.a; // 2

如你所见,我们对 value 的修改悄无声息地失败了。如果我们在 strict mode 下进行尝试,会得到一个错误:

  1. "use strict";
  2. var myObject = {};
  3. Object.defineProperty( myObject, "a", {
  4. value: 2,
  5. writable: false, // 不可写!
  6. configurable: true,
  7. enumerable: true
  8. } );
  9. myObject.a = 3; // TypeError

这个 TypeError 告诉我们,我们不能改变一个不可写属性。

注意: 我们一会儿就会讨论 getters/setters,但是简单地说,你可以观察到 writable:false 意味着值不可改变,和你定义一个空的 setter 是有些等价的。实际上,你的空 setter 在被调用时需要扔出一个 TypeError,来和 writable:false 保持一致。

可配置性(Configurable)

只要属性当前是可配置的,我们就可以使用相同的 defineProperty(..) 工具,修改它的描述符定义。

  1. var myObject = {
  2. a: 2
  3. };
  4. myObject.a = 3;
  5. myObject.a; // 3
  6. Object.defineProperty( myObject, "a", {
  7. value: 4,
  8. writable: true,
  9. configurable: false, // 不可配置!
  10. enumerable: true
  11. } );
  12. myObject.a; // 4
  13. myObject.a = 5;
  14. myObject.a; // 5
  15. Object.defineProperty( myObject, "a", {
  16. value: 6,
  17. writable: true,
  18. configurable: true,
  19. enumerable: true
  20. } ); // TypeError

最后的 defineProperty(..) 调用导致了一个 TypeError,这与 strict mode 无关,如果你试图改变一个不可配置属性的描述符定义,就会发生 TypeError。要小心:如你所看到的,将 configurable 设置为 false一个单向操作,不可撤销!

注意: 这里有一个需要注意的微小例外:即便属性已经是 configurable:falsewritable 总是可以没有错误地从 true 改变为 false,但如果已经是 false 的话不能变回 true

configurable:false 阻止的另外一个事情是使用 delete 操作符移除既存属性的能力。

  1. var myObject = {
  2. a: 2
  3. };
  4. myObject.a; // 2
  5. delete myObject.a;
  6. myObject.a; // undefined
  7. Object.defineProperty( myObject, "a", {
  8. value: 2,
  9. writable: true,
  10. configurable: false,
  11. enumerable: true
  12. } );
  13. myObject.a; // 2
  14. delete myObject.a;
  15. myObject.a; // 2

如你所见,最后的 delete 调用(无声地)失败了,因为我们将 a 属性设置成了不可配置。

delete 仅用于直接从目标对象移除该对象的(可以被移除的)属性。如果一个对象的属性是某个其他对象/函数的最后一个现存的引用,而你 delete 了它,那么这就移除了这个引用,于是现在那个没有被任何地方所引用的对象/函数就可以被作为垃圾回收。但是,将 delete 当做一个像其他语言(如 C/C++)中那样的释放内存工具是 恰当的。delete 仅仅是一个对象属性移除操作 —— 没有更多别的含义。

可枚举性(Enumerable)

我们将要在这里提到的最后一个描述符性质是 enumerable(还有另外两个,我们将在一会儿讨论 getter/setters 时谈到)。

它的名称可能已经使它的功能很明显了,这个性质控制着一个属性是否能在特定的对象-属性枚举操作中出现,比如 for..in 循环。设置为 false 将会阻止它出现在这样的枚举中,即使它依然完全是可以访问的。设置为 true 会使它出现。

所有普通的用户定义属性都默认是可 enumerable 的,正如你通常希望的那样。但如果你有一个特殊的属性,你想让它对枚举隐藏,就将它设置为 enumerable:false

我们一会儿就更加详细地演示可枚举性,所以在大脑中给这个话题上打一个书签。

不可变性(Immutability)

有时我们希望将属性或对象(有意或无意地)设置为不可改变的。ES5 用几种不同的微妙方式,加入了对此功能的支持。

一个重要的注意点是:所有 这些方法创建的都是浅不可变性。也就是,它们仅影响对象和它的直属属性的性质。如果对象拥有对其他对象(数组、对象、函数等)的引用,那个对象的 内容 不会受影响,任然保持可变。

  1. myImmutableObject.foo; // [1,2,3]
  2. myImmutableObject.foo.push( 4 );
  3. myImmutableObject.foo; // [1,2,3,4]

在这段代码中,我们假设 myImmutableObject 已经被创建,而且被保护为不可变。但是,为了保护 myImmutableObject.foo 的内容(也是一个对象 —— 数组),你将需要使用下面的一个或多个方法将 foo 设置为不可变。

注意: 在 JS 程序中创建完全不可动摇的对象是不那么常见的。有些特殊情况当然需要,但作为一个普通的设计模式,如果你发现自己想要 封印(seal)冻结(freeze) 你所有的对象,那么你可能想要退一步来重新考虑你的程序设计,让它对对象值的潜在变化更加健壮。

对象常量(Object Constant)

通过将 writable:falseconfigurable:false 组合,你可以实质上创建了一个作为对象属性的 常量(不能被改变,重定义或删除),比如:

  1. var myObject = {};
  2. Object.defineProperty( myObject, "FAVORITE_NUMBER", {
  3. value: 42,
  4. writable: false,
  5. configurable: false
  6. } );

防止扩展(Prevent Extensions)

如果你想防止一个对象被添加新的属性,但另一方面保留其他既存的对象属性,可以调用 Object.preventExtensions(..)

  1. var myObject = {
  2. a: 2
  3. };
  4. Object.preventExtensions( myObject );
  5. myObject.b = 3;
  6. myObject.b; // undefined

在非 strict mode 模式下,b 的创建会无声地失败。在 strict mode 下,它会抛出 TypeError

封印(Seal)

Object.seal(..) 创建一个“封印”的对象,这意味着它实质上在当前的对象上调用 Object.preventExtensions(..),同时也将它所有的既存属性标记为 configurable:false

所以,你既不能添加更多的属性,也不能重新配置或删除既存属性(虽然你依然 可以 修改它们的值)。

冻结(Freeze)

Object.freeze(..) 创建一个冻结的对象,这意味着它实质上在当前的对象上调用 Object.seal(..),同时也将它所有的“数据访问”属性设置为 writable:false,所以它们的值不可改变。

这种方法是你可以从对象自身获得的最高级别的不可变性,因为它阻止任何对对象或对象直属属性的改变(虽然,就像上面提到的,任何被引用的对象的内容不受影响)。

你可以“深度冻结”一个对象:在这个对象上调用 Object.freeze(..),然后递归地迭代所有它引用的(目前还没有受过影响的)对象,然后也在它们上面调用 Object.freeze(..)。但是要小心,这可能会影响其他你并不打算影响的(共享的)对象。

[[Get]]

关于属性访问如何工作有一个重要的细节。

考虑下面的代码:

  1. var myObject = {
  2. a: 2
  3. };
  4. myObject.a; // 2

myObject.a 是一个属性访问,但是它并不是看起来那样,仅仅在 myObject 中寻找一个名为 a 的属性。

根据语言规范,上面的代码实际上在 myObject 上执行了一个 [[Get]] 操作(有些像 [[Get]]() 函数调用)。对一个对象进行默认的内建 [[Get]] 操作,会 首先 检查对象,寻找一个拥有被请求的名称的属性,如果找到,就返回相应的值。

然而,如果按照被请求的名称 没能 找到属性,[[Get]] 的算法定义了另一个重要的行为。我们会在第五章来解释 接下来 会发生什么(遍历 [[Prototype]] 链,如果有的话)。

[[Get]] 操作的一个重要结果是,如果它通过任何方法都不能找到被请求的属性的值,那么它会返回 undefined

  1. var myObject = {
  2. a: 2
  3. };
  4. myObject.b; // undefined

这个行为和你通过标识符名称来引用 变量 不同。如果你引用了一个在可用的词法作用域内无法解析的变量,其结果不是像对象属性那样返回 undefined,而是抛出一个 ReferenceError

  1. var myObject = {
  2. a: undefined
  3. };
  4. myObject.a; // undefined
  5. myObject.b; // undefined

的角度来说,这两个引用没有区别 —— 它们的结果都是 undefined。然而,在 [[Get]] 操作的底层,虽然不明显,但是比起处理引用 myObject.a,处理 myObject.b 的操作要多做一些潜在的“工作”。

如果仅仅考察结果的值,你无法分辨一个属性是存在并持有一个 undefined 值,还是因为属性根本 存在所以 [[Get]] 无法返回某个具体值而返回默认的 undefined。但是,你很快就能看到你其实 可以 分辨这两种场景。

[[Put]]

既然为了从一个属性中取得值而存在一个内部定义的 [[Get]] 操作,那么很明显应该也存在一个默认的 [[Put]] 操作。

这很容易让人认为,给一个对象的属性赋值,将会在这个对象上调用 [[Put]] 来设置或创建这个属性。但是实际情况却有一些微妙的不同。

调用 [[Put]] 时,它根据几个因素表现不同的行为,包括(影响最大的)属性是否已经在对象中存在了。

如果属性存在,[[Put]] 算法将会大致检查:

  1. 这个属性是访问器描述符吗(见下一节”Getters 与 Setters”)?如果是,而且是 setter,就调用 setter。
  2. 这个属性是 writablefalse 数据描述符吗?如果是,在非 strict mode 下无声地失败,或者在 strict mode 下抛出 TypeError
  3. 否则,像平常一样设置既存属性的值。

如果属性在当前的对象中还不存在,[[Put]] 操作会变得更微妙和复杂。我们将在第五章讨论 [[Prototype]] 时再次回到这个场景,更清楚地解释它。

Getters 与 Setters

对象默认的 [[Put]][[Get]] 操作分别完全控制着如何设置既存或新属性的值,和如何取得既存属性。

注意: 使用较先进的语言特性,覆盖整个对象(不仅是每个属性)的默认 [[Put]][[Get]] 操作是可能的。这超出了我们要在这本书中讨论的范围,但我们会在后面的“你不懂 JS”系列中涵盖此内容。

ES5 引入了一个方法来覆盖这些默认操作的一部分,但不是在对象级别而是针对每个属性,就是通过 getters 和 setters。Getter 是实际上调用一个隐藏函数来取得值的属性。Setter 是实际上调用一个隐藏函数来设置值的属性。

当你将一个属性定义为拥有 getter 或 setter 或两者兼备,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对)。对于访问器描述符,它的 valuewritable 性质因没有意义而被忽略,取而代之的是 JS 将会考虑属性的 setget 性质(还有 configurableenumerable)。

考虑下面的代码:

  1. var myObject = {
  2. // 为 `a` 定义一个 getter
  3. get a() {
  4. return 2;
  5. }
  6. };
  7. Object.defineProperty(
  8. myObject, // 目标对象
  9. "b", // 属性名
  10. { // 描述符
  11. // 为 `b` 定义 getter
  12. get: function(){ return this.a * 2 },
  13. // 确保 `b` 作为对象属性出现
  14. enumerable: true
  15. }
  16. );
  17. myObject.a; // 2
  18. myObject.b; // 4

不管是通过在字面对象语法中使用 get a() { .. },还是通过使用 defineProperty(..) 明确定义,我们都在对象上创建了一个没有实际持有值的属性,访问它们将会自动地对 getter 函数进行隐藏的函数调用,其返回的任何值就是属性访问的结果。

  1. var myObject = {
  2. // 为 `a` 定义 getter
  3. get a() {
  4. return 2;
  5. }
  6. };
  7. myObject.a = 3;
  8. myObject.a; // 2

因为我们仅为 a 定义了一个 getter,如果之后我们试着设置 a 的值,赋值操作并不会抛出错误而是无声地将赋值废弃。就算这里有一个合法的 setter,我们的自定义 getter 将返回值硬编码为仅返回 2,所以赋值操作是没有意义的。

为了使这个场景更合理,正如你可能期望的那样,每个属性还应当被定义一个覆盖默认 [[Put]] 操作(也就是赋值)的 setter。几乎可确定,你将总是想要同时声明 getter 和 setter(仅有它们中的一个经常会导致意外的行为):

  1. var myObject = {
  2. // 为 `a` 定义 getter
  3. get a() {
  4. return this._a_;
  5. },
  6. // 为 `a` 定义 setter
  7. set a(val) {
  8. this._a_ = val * 2;
  9. }
  10. };
  11. myObject.a = 2;
  12. myObject.a; // 4

注意: 在这个例子中,我们实际上将赋值操作([[Put]] 操作)指定的值 2 存储到了另一个变量 _a_ 中。_a_ 这个名称只是用在这个例子中的单纯惯例,并不意味着它的行为有什么特别之处 —— 它和其他普通属性没有区别。

存在性(Existence)

我们早先看到,像 myObject.a 这样的属性访问可能会得到一个 undefined 值,无论是它明确存储着 undefined 还是属性 a 根本就不存在。那么,如果这两种情况的值相同,我们还怎么区别它们呢?

我们可以查询一个对象是否拥有特定的属性,而 不必 取得那个属性的值:

  1. var myObject = {
  2. a: 2
  3. };
  4. ("a" in myObject); // true
  5. ("b" in myObject); // false
  6. myObject.hasOwnProperty( "a" ); // true
  7. myObject.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] 这样的检查,但是这总是不像我们想象的那样工作。

枚举(Enumeration)

先前,在学习 enumerable 属性描述符性质时,我们简单地解释了”可枚举性(enumerability)”的含义。现在,让我们来更加详细地重新讲解它。

  1. var myObject = { };
  2. Object.defineProperty(
  3. myObject,
  4. "a",
  5. // 使 `a` 可枚举,如一般情况
  6. { enumerable: true, value: 2 }
  7. );
  8. Object.defineProperty(
  9. myObject,
  10. "b",
  11. // 使 `b` 不可枚举
  12. { enumerable: false, value: 3 }
  13. );
  14. myObject.b; // 3
  15. ("b" in myObject); // true
  16. myObject.hasOwnProperty( "b" ); // true
  17. // .......
  18. for (var k in myObject) {
  19. console.log( k, myObject[k] );
  20. }
  21. // "a" 2

你会注意到,myObject.b 实际上 存在,而且拥有可以访问的值,但是它不出现在 for..in 循环中(然而令人诧异的是,它的 in 操作符的存在性检查通过了)。这是因为 “enumerable” 基本上意味着“如果对象的属性被迭代时会被包含在内”。

注意:for..in 循环实施在数组上可能会给出意外的结果,因为枚举一个数组将不仅包含所有的数字下标,还包含所有的可枚举属性。所以一个好主意是:将 for..in 循环 用于对象,而为存储在数组中的值使用传统的 for 循环并用数字索引迭代。

另一个可以区分可枚举和不可枚举属性的方法是:

  1. var myObject = { };
  2. Object.defineProperty(
  3. myObject,
  4. "a",
  5. // 使 `a` 可枚举,如一般情况
  6. { enumerable: true, value: 2 }
  7. );
  8. Object.defineProperty(
  9. myObject,
  10. "b",
  11. // 使 `b` 不可枚举
  12. { enumerable: false, value: 3 }
  13. );
  14. myObject.propertyIsEnumerable( "a" ); // true
  15. myObject.propertyIsEnumerable( "b" ); // false
  16. Object.keys( myObject ); // ["a"]
  17. Object.getOwnPropertyNames( myObject ); // ["a", "b"]

propertyIsEnumerable(..) 测试一个给定的属性名是否直 接存 在于对象上,并且是 enumerable:true

Object.keys(..) 返回一个所有可枚举属性的数组,而 Object.getOwnPropertyNames(..) 返回一个 所有 属性的数组,不论能不能枚举。

inhasOwnProperty(..) 区别于它们是否查询 [[Prototype]] 链,而 Object.keys(..)Object.getOwnPropertyNames(..) 考察直接给定的对象。

(当下)没有与 in 操作符的查询方式(在整个 [[Prototype]] 链上遍历所有的属性,如我们在第五章解释的)等价的、内建的方法可以得到一个 所有属性 的列表。你可以近似地模拟一个这样的工具:递归地遍历一个对象的 [[Prototype]] 链,在每一层都从 Object.keys(..) 中取得一个列表——仅包含可枚举属性。