深入理解 JavaScript
请支持本书:购买捐赠
(广告,请不要屏蔽。)

12 属性的可枚举性



可枚举性是对象属性的一个*特性*。在本章中,我们将仔细研究它的用途以及它如何影响 Object.keys()Object.assign() 等操作。

  必备知识:属性特性

对于本章,您应该熟悉属性特性。如果您不熟悉,请查看§9 “属性特性:简介”

12.1 可枚举性如何影响属性迭代结构

为了演示各种操作如何受可枚举性的影响,我们使用以下原型为 proto 的对象 obj

const protoEnumSymbolKey = Symbol('protoEnumSymbolKey');
const protoNonEnumSymbolKey = Symbol('protoNonEnumSymbolKey');
const proto = Object.defineProperties({}, {
  protoEnumStringKey: {
    value: 'protoEnumStringKeyValue',
    enumerable: true,
  },
  [protoEnumSymbolKey]: {
    value: 'protoEnumSymbolKeyValue',
    enumerable: true,
  },
  protoNonEnumStringKey: {
    value: 'protoNonEnumStringKeyValue',
    enumerable: false,
  },
  [protoNonEnumSymbolKey]: {
    value: 'protoNonEnumSymbolKeyValue',
    enumerable: false,
  },
});

const objEnumSymbolKey = Symbol('objEnumSymbolKey');
const objNonEnumSymbolKey = Symbol('objNonEnumSymbolKey');
const obj = Object.create(proto, {
  objEnumStringKey: {
    value: 'objEnumStringKeyValue',
    enumerable: true,
  },
  [objEnumSymbolKey]: {
    value: 'objEnumSymbolKeyValue',
    enumerable: true,
  },
  objNonEnumStringKey: {
    value: 'objNonEnumStringKeyValue',
    enumerable: false,
  },
  [objNonEnumSymbolKey]: {
    value: 'objNonEnumSymbolKeyValue',
    enumerable: false,
  },
});

12.1.1 仅考虑可枚举属性的操作

表 2:忽略不可枚举属性的操作。
操作 字符串键 符号键 继承的
Object.keys() ES5
Object.values() ES2017
Object.entries() ES2017
展开 {...x} ES2018
Object.assign() ES6
JSON.stringify() ES5
for-in ES1

以下操作(总结在表 2 中)仅考虑可枚举属性

for-in 是唯一一个可枚举性对继承属性重要的内置操作。所有其他操作仅适用于自身属性。

12.1.2 同时考虑可枚举和不可枚举属性的操作

表 3:同时考虑可枚举和不可枚举属性的操作。
操作 字符串键 符号键 继承的
Object.getOwnPropertyNames() ES5
Object.getOwnPropertySymbols() ES6
Reflect.ownKeys() ES6
Object.getOwnPropertyDescriptors() ES2017

以下操作(总结在表 3 中)同时考虑可枚举和不可枚举属性

12.1.3 自省操作的命名规则

*自省*使程序能够在运行时检查值的结构。它是*元编程*:普通编程是关于编写程序;元编程是关于检查和/或更改程序。

在 JavaScript 中,常见的自省操作名称较短,而很少使用的操作名称较长。忽略不可枚举属性是常态,这就是为什么执行此操作的操作名称较短,而其他操作名称较长的原因

但是,Reflect 方法(例如 Reflect.ownKeys())偏离了此规则,因为 Reflect 提供了与代理相关的更多“元”操作。

此外,还进行了以下区分(自 ES6 引入符号以来)

因此,Object.keys() 的更好名称现在是 Object.names()

12.2 预定义和创建属性的可枚举性

在本节中,我们将像这样缩写 Object.getOwnPropertyDescriptor()

const desc = Object.getOwnPropertyDescriptor.bind(Object);

大多数数据属性是使用以下特性创建的

{
  writable: true,
  enumerable: false,
  configurable: true,
}

这包括

最重要的不可枚举属性是

接下来,我们将研究可枚举性的用例,这将告诉我们为什么某些属性是可枚举的,而另一些则不是。

12.3 可枚举性的用例

可枚举性是一个不一致的特性。它确实有用例,但总是有一些警告。在本节中,我们将研究用例和警告。

12.3.1 用例:在 for-in 循环中隐藏属性

for-in 循环遍历对象的*所有*可枚举字符串键属性,包括自身和继承的属性。因此,特性 enumerable 用于隐藏不应遍历的属性。这就是在 ECMAScript 1 中引入可枚举性的原因。

一般来说,最好避免使用 for-in。接下来的两小节将解释原因。以下函数将帮助我们演示 for-in 的工作原理。

function listPropertiesViaForIn(obj) {
  const result = [];
  for (const key in obj) {
    result.push(key);
  }
  return result;
}
12.3.1.1 对对象使用 for-in 的警告

for-in 迭代所有属性,包括继承的属性

const proto = {enumerableProtoProp: 1};
const obj = {
  __proto__: proto,
  enumerableObjProp: 2,
};
assert.deepEqual(
  listPropertiesViaForIn(obj),
  ['enumerableObjProp', 'enumerableProtoProp']);

对于普通的普通对象,for-in 不会看到继承的方法,例如 Object.prototype.toString(),因为它们都是不可枚举的

const obj = {};
assert.deepEqual(
  listPropertiesViaForIn(obj),
  []);

在用户定义的类中,所有继承的属性也是不可枚举的,因此会被忽略

class Person {
  constructor(first, last) {
    this.first = first;
    this.last = last;
  }
  getName() {
    return this.first + ' ' + this.last;
  }
}
const jane = new Person('Jane', 'Doe');
assert.deepEqual(
  listPropertiesViaForIn(jane),
  ['first', 'last']);

**结论:**在对象中,for-in 会考虑继承的属性,而我们通常希望忽略这些属性。在这种情况下,最好将 for-of 循环与 Object.keys()Object.entries() 等结合使用。

12.3.1.2 对数组使用 for-in 的警告

自身属性 .length 在数组和字符串中是不可枚举的,因此会被 for-in 忽略

> listPropertiesViaForIn(['a', 'b'])
[ '0', '1' ]
> listPropertiesViaForIn('ab')
[ '0', '1' ]

但是,通常不建议使用 for-in 迭代数组的索引,因为它会同时考虑不是索引的继承属性和自身属性。以下示例演示了如果数组具有自身非索引属性会发生什么

const arr1 = ['a', 'b'];
assert.deepEqual(
  listPropertiesViaForIn(arr1),
  ['0', '1']);

const arr2 = ['a', 'b'];
arr2.nonIndexProp = 'yes';
assert.deepEqual(
  listPropertiesViaForIn(arr2),
  ['0', '1', 'nonIndexProp']);

**结论:**不应使用 for-in 迭代数组的索引,因为它会同时考虑索引属性和非索引属性

12.3.2 用例:将属性标记为不可复制

通过将属性设置为不可枚举,我们可以将它们隐藏在某些复制操作中。在继续介绍更现代的复制操作之前,让我们首先检查两个历史复制操作。

12.3.2.1 历史复制操作:Prototype 的 Object.extend()

Prototype 是一个 JavaScript 框架,由 Sam Stephenson 于 2005 年 2 月创建,作为 Ruby on Rails 中 Ajax 支持的基础。

Prototype 的 Object.extend(destination, source)source 的所有可枚举自身和继承属性复制到 destination 的自身属性中。它的实现方式如下

function extend(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
}

如果我们将 Object.extend() 与对象一起使用,我们可以看到它将继承的属性复制到自身属性中,并忽略不可枚举的属性(它也忽略符号键属性)。所有这些都是由于 for-in 的工作原理造成的。

const proto = Object.defineProperties({}, {
  enumProtoProp: {
    value: 1,
    enumerable: true,
  },
  nonEnumProtoProp: {
    value: 2,
    enumerable: false,
  },
});
const obj = Object.create(proto, {
  enumObjProp: {
    value: 3,
    enumerable: true,
  },
  nonEnumObjProp: {
    value: 4,
    enumerable: false,
  },
});

assert.deepEqual(
  extend({}, obj),
  {enumObjProp: 3, enumProtoProp: 1});
12.3.2.2 历史复制操作:jQuery 的 $.extend()

jQuery 的 $.extend(target, source1, source2, ···) 的工作原理类似于 Object.extend()

12.3.2.3 可枚举性驱动复制的缺点

基于可枚举性进行复制有几个缺点

12.3.2.4 Object.assign() [ES5]

在 ES6 中,Object.assign(target, source_1, source_2, ···) 可用于将源合并到目标中。会考虑源的所有自身可枚举属性(具有字符串键或符号键)。Object.assign() 使用“get”操作从源读取值,并使用“set”操作将值写入目标。

关于可枚举性,Object.assign() 延续了 Object.extend()$.extend() 的传统。引用 Yehuda Katz 的话

Object.assign 将为所有现有的 extend() API 铺平道路。我们认为在这些情况下不复制可枚举方法的先例足以让 Object.assign 具有这种行为。

换句话说:创建 Object.assign() 时考虑到了从 $.extend()(及类似方法)升级的路径。它的方法比 $.extend 更简洁,因为它忽略了继承的属性。

12.3.2.5 复制时不可枚举性有用的罕见示例

不可枚举性有帮助的情况很少。一个罕见的例子是fs-extra 最近遇到的一个问题

12.3.3 将属性标记为私有

如果我们将属性设置为不可枚举,则 Object.keys()for-in 循环等将无法再看到它。对于这些机制,该属性是私有的。

但是,这种方法存在几个问题

12.3.4 在 JSON.stringify() 中隐藏自身属性

JSON.stringify() 的输出中不包含不可枚举的属性。因此,我们可以使用可枚举性来确定哪些自身属性应该导出到 JSON。此用例与前一个用例类似,即将属性标记为私有。但它也有所不同,因为它更多地是关于导出,并且适用略有不同的注意事项。例如:对象可以从 JSON 完全重建吗?

作为可枚举性的替代方法,对象可以实现 .toJSON() 方法,并且 JSON.stringify() 会将该方法返回的内容(而不是对象本身)字符串化。下面的示例演示了它是如何工作的。

class Point {
  static fromJSON(json) {
    return new Point(json[0], json[1]);
  }
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toJSON() {
    return [this.x, this.y];
  }
}
assert.equal(
  JSON.stringify(new Point(8, -3)),
  '[8,-3]'
);

我发现 toJSON() 比可枚举性更简洁。它还为我们提供了更多关于存储格式的自由度。

12.4 结论

我们已经看到,几乎所有不可枚举的应用都是现在有其他更好解决方案的变通方法。

对于我们自己的代码,我们通常可以假装可枚举性不存在

也就是说,我们会自动遵循最佳实践。