Object.create()
:通过描述符创建对象Object.getOwnPropertyDescriptors()
的用例.length
在本章中,我们将仔细研究 ECMAScript 规范如何看待 JavaScript 对象。特别是,属性在规范中不是原子的,而是由多个*属性*(想想记录中的字段)组成的。即使是数据属性的值也存储在一个属性中!
在 ECMAScript 规范中,一个对象由以下部分组成
规范对内部插槽的描述如下。我添加了项目符号并强调了一部分
undefined
。[[ ]]
中的名称来标识。有两种内部插槽
普通对象具有以下数据插槽
.[[Prototype]]: null | object
Object.getPrototypeOf()
和 Object.setPrototypeOf()
间接访问。.[[Extensible]]: boolean
Object.preventExtensions()
设置为 false
。.[[PrivateFieldValues]]: EntryList
属性的键可以是
有两种属性,它们的特征在于它们的属性
value
保存任何 JavaScript 值。get
中,后者存储在属性 set
中。此外,还有两种属性都具有的属性。下表列出了所有属性及其默认值。
属性类型 | 属性名称和类型 | 默认值 |
---|---|---|
数据属性 | value: any |
undefined |
writable: boolean |
false |
|
访问器属性 | get: (this: any) => any |
undefined |
set: (this: any, v: any) => void |
undefined |
|
所有属性 | configurable: boolean |
false |
enumerable: boolean |
false |
我们已经遇到了 value
、get
和 set
属性。其他属性的工作原理如下
writable
确定是否可以更改数据属性的值。configurable
确定是否可以更改属性的属性。如果它是 false
,则value
之外的任何属性。writable
从 true
更改为 false
。这种异常背后的理由是 历史原因:数组的属性 .length
一直是可写的和不可配置的。允许更改其 writable
属性使我们能够冻结数组。enumerable
影响某些操作(例如 Object.keys()
)。如果它是 false
,则这些操作会忽略该属性。大多数属性都是可枚举的(例如,通过赋值或对象字面量创建的属性),这就是为什么您在实践中很少注意到此属性的原因。如果您仍然对它的工作原理感兴趣,请参阅 §12 “属性的可枚举性”。如果继承的属性是不可写的,我们不能使用赋值来创建具有相同键的自有属性
const proto = {
prop: 1,
};
// Make proto.prop non-writable:
Object.defineProperty(
proto, 'prop', {writable: false});
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 2,
/^TypeError: Cannot assign to read only property 'prop'/);
有关更多信息,请参阅 §11.3.4 “继承的只读属性阻止通过赋值创建自有属性”。
*属性描述符*将属性的属性编码为 JavaScript 对象。它们的 TypeScript 接口如下所示。
interface DataPropertyDescriptor {
value?: any;
writable?: boolean;
configurable?: boolean;
enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
get?: (this: any) => any;
set?: (this: any, v: any) => void;
configurable?: boolean;
enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;
问号表示所有属性都是可选的。 §9.7 “省略描述符属性” 描述了如果省略它们会发生什么。
Object.getOwnPropertyDescriptor()
:检索单个属性的描述符考虑以下对象
const legoBrick = {
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
};
让我们首先获取数据属性 .color
的描述符
assert.deepEqual(
Object.getOwnPropertyDescriptor(legoBrick, 'color'),
{
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
});
这就是访问器属性 .description
的描述符的样子
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptor(legoBrick, 'description'),
{
get: desc(legoBrick, 'description').get, // (A)
set: undefined,
enumerable: true,
configurable: true
});
在 A 行使用实用函数 desc()
可确保 .deepEqual()
工作。
Object.getOwnPropertyDescriptors()
:检索对象所有属性的描述符const legoBrick = {
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptors(legoBrick),
{
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: desc(legoBrick, 'description').get, // (A)
set: undefined,
enumerable: true,
configurable: true,
},
});
在 A 行使用辅助函数 desc()
可确保 .deepEqual()
工作。
如果我们通过属性描述符 propDesc
定义键为 k
的属性,则会发生什么取决于
k
的属性,则会创建一个新的自有属性,该属性具有 propDesc
指定的属性。k
的属性,则定义会更改属性的属性,使其与 propDesc
匹配。Object.defineProperty()
:通过描述符定义单个属性首先,让我们通过描述符创建一个新属性
const car = {};
Object.defineProperty(car, 'color', {
value: 'blue',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
car,
{
color: 'blue',
});
接下来,我们通过描述符更改属性的类型;我们将数据属性转换为 getter
const car = {
color: 'blue',
};
let readCount = 0;
Object.defineProperty(car, 'color', {
get() {
readCount++;
return 'red';
},
});
assert.equal(car.color, 'red');
assert.equal(readCount, 1);
最后,我们通过描述符更改数据属性的值
const car = {
color: 'blue',
};
// Use the same attributes as assignment:
Object.defineProperty(
car, 'color', {
value: 'green',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
car,
{
color: 'green',
});
我们使用了与赋值相同的属性属性。
Object.defineProperties()
:通过描述符定义多个属性Object.defineProperties()
是 `Object.defineProperty()
的多属性版本
const legoBrick1 = {};
Object.defineProperties(
legoBrick1,
{
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: function () {
return `${this.kind} (${this.color})`;
},
enumerable: true,
configurable: true,
},
});
assert.deepEqual(
legoBrick1,
{
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
});
Object.create()
:通过描述符创建对象Object.create()
创建一个新对象。它的第一个参数指定该对象的原型。它的可选第二个参数指定该对象的属性的描述符。在下一个示例中,我们创建与上一个示例相同的对象。
const legoBrick2 = Object.create(
Object.prototype,
{
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: function () {
return `${this.kind} (${this.color})`;
},
enumerable: true,
configurable: true,
},
});
// Did we really create the same object?
assert.deepEqual(legoBrick1, legoBrick2); // Yes!
Object.getOwnPropertyDescriptors()
的用例如果我们将 Object.getOwnPropertyDescriptors()
与 Object.defineProperties()
或 Object.create()
结合使用,它可以帮助我们处理两个用例。
自 ES6 以来,JavaScript 已经有一个用于复制属性的工具方法:Object.assign()
。但是,此方法使用简单的 get 和 set 操作来复制键为 key
的属性
这意味着它仅在以下情况下才会创建属性的忠实副本:
writable
为 true
且其属性 enumerable
为 true
(因为这就是赋值创建属性的方式)。以下示例说明了此限制。对象 source
有一个键为 data
的 setter。
const source = {
set data(value) {
this._data = value;
}
};
// Property `data` exists because there is only a setter
// but has the value `undefined`.
assert.equal('data' in source, true);
assert.equal(source.data, undefined);
如果我们使用 Object.assign()
复制属性 data
,则访问器属性 data
将转换为数据属性
const target1 = {};
Object.assign(target1, source);
assert.deepEqual(
Object.getOwnPropertyDescriptor(target1, 'data'),
{
value: undefined,
writable: true,
enumerable: true,
configurable: true,
});
// For comparison, the original:
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptor(source, 'data'),
{
get: undefined,
set: desc(source, 'data').set,
enumerable: true,
configurable: true,
});
幸运的是,将 Object.getOwnPropertyDescriptors()
与 Object.defineProperties()
一起使用可以忠实地复制属性 data
const target2 = {};
Object.defineProperties(
target2, Object.getOwnPropertyDescriptors(source));
assert.deepEqual(
Object.getOwnPropertyDescriptor(target2, 'data'),
{
get: undefined,
set: desc(source, 'data').set,
enumerable: true,
configurable: true,
});
super
的方法使用 super
的方法与其*宿主对象*(存储它的对象)紧密相连。目前没有办法将此类方法复制或移动到不同的对象。
Object.getOwnPropertyDescriptors()
的用例:克隆对象浅克隆类似于复制属性,这就是为什么 Object.getOwnPropertyDescriptors()
在这里也是一个不错的选择。
要创建克隆,我们使用 Object.create()
const original = {
set data(value) {
this._data = value;
}
};
const clone = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original));
assert.deepEqual(original, clone);
有关此主题的更多信息,请参阅 §6 “复制对象和数组”。
描述符的所有属性都是可选的。省略属性时会发生什么取决于操作。
当我们通过描述符创建新属性时,省略属性意味着将使用它们的默认值
const car = {};
Object.defineProperty(
car, 'color', {
value: 'red',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'red',
writable: false,
enumerable: false,
configurable: false,
});
相反,如果我们更改现有属性,则省略描述符属性意味着不会触及相应的属性
const car = {
color: 'yellow',
};
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
});
Object.defineProperty(
car, 'color', {
value: 'pink',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'pink',
writable: true,
enumerable: true,
configurable: true,
});
属性属性的一般规则(几乎没有例外)是
原型链开头处的对象的属性通常是可写的、可枚举的和可配置的。
正如关于可枚举性的章节中所述,大多数继承的属性都是不可枚举的,以便将它们从 for-in
循环等遗留结构中隐藏起来。继承的属性通常是可写的和可配置的。
const obj = {};
obj.prop = 3;
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
}
});
const obj = { prop: 'yes' };
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 'yes',
writable: true,
enumerable: true,
configurable: true
}
});
.length
数组的自有属性 .length
是不可枚举的,因此它不会被 Object.assign()
、展开运算符和类似操作复制。它也是不可配置的。
> Object.getOwnPropertyDescriptor([], 'length')
{ value: 0, writable: true, enumerable: false, configurable: false }
> Object.getOwnPropertyDescriptor('abc', 'length')
{ value: 3, writable: false, enumerable: false, configurable: false }
.length
是一种特殊的数据属性,因为它受其他自有属性(特别是索引属性)的影响(并且也会影响其他自有属性)。
assert.deepEqual(
Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
{
value: Array.prototype.map,
writable: true,
enumerable: false,
configurable: true
});
class DataContainer {
accessCount = 0;
constructor(data) {
this.data = data;
}
getData() {
this.accessCount++;
return this.data;
}
}
assert.deepEqual(
Object.getOwnPropertyDescriptors(DataContainer.prototype),
{
constructor: {
value: DataContainer,
writable: true,
enumerable: false,
configurable: true,
},
getData: {
value: DataContainer.prototype.getData,
writable: true,
enumerable: false,
configurable: true,
}
});
请注意,DataContainer
实例的所有自有属性都是可写的、可枚举的和可配置的。
const dc = new DataContainer('abc')
assert.deepEqual(
Object.getOwnPropertyDescriptors(dc),
{
accessCount: {
value: 0,
writable: true,
enumerable: true,
configurable: true,
},
data: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true,
}
});
以下工具方法使用属性描述符:
Object.defineProperty(obj: object, key: string|symbol, propDesc: PropertyDescriptor): object
[ES5]
在 obj
上创建或更改一个属性,其键为 key
,其属性通过 propDesc
指定。返回修改后的对象。
Object.defineProperties(obj: object, properties: {[k: string|symbol]: PropertyDescriptor}): object
[ES5]
Object.defineProperty()
的批量版本。对象 properties
的每个属性 p
指定要添加到 obj
的一个属性:p
的键指定属性的键,p
的值是一个描述符,用于指定属性的特性。
Object.create(proto: null|object, properties?: {[k: string|symbol]: PropertyDescriptor}): object
[ES5]
首先,创建一个原型为 proto
的对象。然后,如果提供了可选参数 properties
,则向其添加属性,方式与 Object.defineProperties()
相同。最后,返回结果。例如,以下代码片段与前一个代码片段产生相同的结果:
Object.getOwnPropertyDescriptor(obj: object, key: string|symbol): undefined|PropertyDescriptor
[ES5]
返回 obj
的自有(非继承)属性的描述符,其键为 key
。如果没有这样的属性,则返回 undefined
。
Object.getOwnPropertyDescriptors(obj: object): {[k: string|symbol]: PropertyDescriptor}
[ES2017]
返回一个对象,其中 obj
的每个属性键 'k'
都映射到 obj.k
的属性描述符。结果可以用作 Object.defineProperties()
和 Object.create()
的输入。
const propertyKey = Symbol('propertyKey');
const obj = {
[propertyKey]: 'abc',
get count() { return 123 },
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
[propertyKey]: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true
},
count: {
get: desc(obj, 'count').get, // (A)
set: undefined,
enumerable: true,
configurable: true
}
});
在 A 行中使用 desc()
是一个变通方法,以便 .deepEqual()
可以正常工作。
接下来的三章将提供有关属性特性的更多详细信息: