for-in
循环中隐藏属性JSON.stringify()
中隐藏自身属性可枚举性是对象属性的一个*特性*。在本章中,我们将仔细研究它的用途以及它如何影响 Object.keys()
和 Object.assign()
等操作。
必备知识:属性特性
对于本章,您应该熟悉属性特性。如果您不熟悉,请查看§9 “属性特性:简介”。
为了演示各种操作如何受可枚举性的影响,我们使用以下原型为 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,
},
});
操作 | 字符串键 | 符号键 | 继承的 | |
---|---|---|---|---|
Object.keys() |
ES5 | ✔ |
✘ |
✘ |
Object.values() |
ES2017 | ✔ |
✘ |
✘ |
Object.entries() |
ES2017 | ✔ |
✘ |
✘ |
展开 {...x} |
ES2018 | ✔ |
✔ |
✘ |
Object.assign() |
ES6 | ✔ |
✔ |
✘ |
JSON.stringify() |
ES5 | ✔ |
✘ |
✘ |
for-in |
ES1 | ✔ |
✘ |
✔ |
以下操作(总结在表 2 中)仅考虑可枚举属性
Object.keys()
[ES5] 返回可枚举自身字符串键属性的键。
Object.values()
[ES2017] 返回可枚举自身字符串键属性的值。
Object.entries()
[ES2017] 返回可枚举自身字符串键属性的键值对。(请注意,Object.fromEntries()
确实接受符号作为键,但仅创建可枚举属性。)
展开到对象字面量 [ES2018] 仅考虑自身可枚举属性(具有字符串键或符号键)。
Object.assign()
[ES6] 仅复制可枚举自身属性(具有字符串键或符号键)。
JSON.stringify()
[ES5] 仅将具有字符串键的可枚举自身属性字符串化。
for-in
循环 [ES1] 遍历自身和继承的可枚举字符串键属性的键。
for-in
是唯一一个可枚举性对继承属性重要的内置操作。所有其他操作仅适用于自身属性。
操作 | 字符串键 | 符号键 | 继承的 | |
---|---|---|---|---|
Object.getOwnPropertyNames() |
ES5 | ✔ |
✘ |
✘ |
Object.getOwnPropertySymbols() |
ES6 | ✘ |
✔ |
✘ |
Reflect.ownKeys() |
ES6 | ✔ |
✔ |
✘ |
Object.getOwnPropertyDescriptors() |
ES2017 | ✔ |
✔ |
✘ |
以下操作(总结在表 3 中)同时考虑可枚举和不可枚举属性
Object.getOwnPropertyNames()
[ES5] 列出所有自身字符串键属性的键。
Object.getOwnPropertySymbols()
[ES6] 列出所有自身符号键属性的键。
Reflect.ownKeys()
[ES6] 列出所有自身属性的键。
Object.getOwnPropertyDescriptors()
[ES2017] 列出所有自身属性的属性描述符。
> Object.getOwnPropertyDescriptors(obj)
{
objEnumStringKey: {
value: 'objEnumStringKeyValue',
writable: false,
enumerable: true,
configurable: false
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
writable: false,
enumerable: false,
configurable: false
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
writable: false,
enumerable: true,
configurable: false
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
writable: false,
enumerable: false,
configurable: false
}
}
*自省*使程序能够在运行时检查值的结构。它是*元编程*:普通编程是关于编写程序;元编程是关于检查和/或更改程序。
在 JavaScript 中,常见的自省操作名称较短,而很少使用的操作名称较长。忽略不可枚举属性是常态,这就是为什么执行此操作的操作名称较短,而其他操作名称较长的原因
Object.keys()
忽略不可枚举属性。Object.getOwnPropertyNames()
列出所有自身属性的字符串键。但是,Reflect
方法(例如 Reflect.ownKeys()
)偏离了此规则,因为 Reflect
提供了与代理相关的更多“元”操作。
此外,还进行了以下区分(自 ES6 引入符号以来)
因此,Object.keys()
的更好名称现在是 Object.names()
。
在本节中,我们将像这样缩写 Object.getOwnPropertyDescriptor()
大多数数据属性是使用以下特性创建的
这包括
Object.fromEntries()
最重要的不可枚举属性是
内置类的原型属性
通过用户定义的类创建的原型属性
数组的属性 .length
字符串的属性 .length
(请注意,原始值的所有属性都是只读的)
接下来,我们将研究可枚举性的用例,这将告诉我们为什么某些属性是可枚举的,而另一些则不是。
可枚举性是一个不一致的特性。它确实有用例,但总是有一些警告。在本节中,我们将研究用例和警告。
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;
}
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()
,因为它们都是不可枚举的
在用户定义的类中,所有继承的属性也是不可枚举的,因此会被忽略
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()
等结合使用。
for-in
的警告自身属性 .length
在数组和字符串中是不可枚举的,因此会被 for-in
忽略
但是,通常不建议使用 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
迭代数组的索引,因为它会同时考虑索引属性和非索引属性
如果您对数组的键感兴趣,请使用数组方法 .keys()
如果要迭代数组的元素,请使用 for-of
循环,它的另一个好处是可以与其他可迭代数据结构一起使用。
通过将属性设置为不可枚举,我们可以将它们隐藏在某些复制操作中。在继续介绍更现代的复制操作之前,让我们首先检查两个历史复制操作。
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});
$.extend()
jQuery 的 $.extend(target, source1, source2, ···)
的工作原理类似于 Object.extend()
source1
的所有可枚举自身和继承属性复制到 target
的自身属性中。source2
执行相同的操作。基于可枚举性进行复制有几个缺点
虽然可枚举性对于隐藏继承的属性很有用,但主要以这种方式使用它是为了通常只将自身属性复制到自身属性中。通过忽略继承的属性可以更好地实现相同的效果。
复制哪些属性通常取决于手头的任务;为所有用例设置单个标志几乎没有意义。更好的选择是为复制操作提供一个*谓词*(一个返回布尔值的回调),告诉它何时忽略属性。
可枚举性在复制时方便地隐藏了数组的自身属性 .length
。但这是一种极其罕见的例外情况:一个神奇的属性,它既影响兄弟属性,又受兄弟属性的影响。如果我们自己实现这种魔法,我们将使用(继承的)getter 和/或 setter,而不是(自身的)数据属性。
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
更简洁,因为它忽略了继承的属性。
不可枚举性有帮助的情况很少。一个罕见的例子是库 fs-extra
最近遇到的一个问题
内置 Node.js 模块 fs
有一个属性 .promises
,其中包含一个具有 fs
API 的基于 Promise 版本的对象。在出现问题时,读取 .promise
会导致以下警告记录到控制台
ExperimentalWarning: The fs.promises API is experimental
除了提供自身功能外,fs-extra
还重新导出 fs
中的所有内容。对于 CommonJS 模块,这意味着将 fs
的所有属性复制到 fs-extra
的 module.exports
中(通过 Object.assign()
)。当 fs-extra
这样做时,它会触发警告。这令人困惑,因为它在每次加载 fs-extra
时都会发生。
一个快速解决方案快速解决方案是使属性 fs.promises
不可枚举。之后,fs-extra
就会忽略它。
如果我们将属性设置为不可枚举,则 Object.keys()
、for-in
循环等将无法再看到它。对于这些机制,该属性是私有的。
但是,这种方法存在几个问题
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()
比可枚举性更简洁。它还为我们提供了更多关于存储格式的自由度。
我们已经看到,几乎所有不可枚举的应用都是现在有其他更好解决方案的变通方法。
对于我们自己的代码,我们通常可以假装可枚举性不存在
也就是说,我们会自动遵循最佳实践。