(广告,请勿屏蔽。)
9 属性属性:简介
9.1 对象的结构
9.1.1 内部插槽
9.1.2 属性键
9.1.3 属性属性
9.2 属性描述符
9.3 检索属性的描述符
9.3.1 Object.getOwnPropertyDescriptor():检索单个属性的描述符
9.3.2 Object.getOwnPropertyDescriptors():检索对象所有属性的描述符
9.4 通过描述符定义属性
9.4.1 Object.defineProperty():通过描述符定义单个属性
9.4.2 Object.defineProperties():通过描述符定义多个属性
9.5 Object.create():通过描述符创建对象
9.6 Object.getOwnPropertyDescriptors() 的用例
9.6.1 用例:将属性复制到对象中
9.6.2 Object.getOwnPropertyDescriptors() 的用例:克隆对象
9.7 省略描述符属性
9.7.1 创建属性时省略描述符属性
9.7.2 更改属性时省略描述符属性
9.8 内置构造函数使用哪些属性属性?
9.8.1 通过赋值创建的自有属性
9.8.2 通过对象字面量创建的自有属性
9.8.3 数组的自有属性 .length
9.8.4 内置类的原型属性
9.8.5 用户定义类的原型属性和实例属性
9.9 API:属性描述符
9.10 进一步阅读
在本章中,我们将仔细研究 ECMAScript 规范如何看待 JavaScript 对象。特别是,属性在规范中不是原子的,而是由多个*属性*(想想记录中的字段)组成的。即使是数据属性的值也存储在一个属性中!
9.1 对象的结构
在 ECMAScript 规范中,一个对象由以下部分组成
*内部插槽*,它们是不可从 JavaScript 访问的存储位置,只能从规范中的操作访问。
一个*属性*集合。每个属性都将一个*键*与*属性*(想想记录中的字段)相关联。
9.1.1 内部插槽
规范对内部插槽的描述如下。我添加了项目符号并强调了一部分
内部插槽对应于与对象关联的内部状态,并由各种 ECMAScript 规范算法使用。
内部插槽不是对象属性,它们不会被继承。
根据特定的内部插槽规范,此类状态可能包含
任何 ECMAScript 语言类型的值,或
特定 ECMAScript 规范类型的值。
除非另有明确说明,否则内部插槽是在创建对象的过程中分配的,并且不能动态添加到对象中。
除非另有说明,否则内部插槽的初始值为 undefined。
此规范中的各种算法都会创建具有内部插槽的对象。但是,**ECMAScript 语言没有提供将内部插槽与对象相关联的直接方法**。
内部方法和内部插槽在此规范中使用括在双括号 [[ ]] 中的名称来标识。
有两种内部插槽
用于操作对象的方法插槽(获取属性、设置属性等)
存储值的数据插槽。
普通对象具有以下数据插槽
.[[Prototype]]: null | object
存储对象的原型。
可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 间接访问。
.[[Extensible]]: boolean
指示是否可以向对象添加属性。
可以通过 Object.preventExtensions() 设置为 false。
.[[PrivateFieldValues]]: EntryList
用于管理 私有类字段。
9.1.2 属性键
属性的键可以是
字符串
符号
9.1.3 属性属性
有两种属性,它们的特征在于它们的属性
*数据属性*存储数据。它的属性 value 保存任何 JavaScript 值。
*访问器属性*由 getter 函数和/或 setter 函数组成。前者存储在属性 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 “属性的可枚举性”。
9.1.3.1 陷阱:继承的不可写属性阻止通过赋值创建自有属性
如果继承的属性是不可写的,我们不能使用赋值来创建具有相同键的自有属性
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 “继承的只读属性阻止通过赋值创建自有属性”。
9.2 属性描述符
*属性描述符*将属性的属性编码为 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 “省略描述符属性” 描述了如果省略它们会发生什么。
9.3 检索属性的描述符
9.3.1 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() 工作。
9.3.2 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() 工作。
9.4 通过描述符定义属性
如果我们通过属性描述符 propDesc 定义键为 k 的属性,则会发生什么取决于
如果没有键为 k 的属性,则会创建一个新的自有属性,该属性具有 propDesc 指定的属性。
如果有键为 k 的属性,则定义会更改属性的属性,使其与 propDesc 匹配。
9.4.1 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',
});
我们使用了与赋值相同的属性属性。
9.4.2 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})`;
},
});
9.5 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!
9.6 Object.getOwnPropertyDescriptors() 的用例
如果我们将 Object.getOwnPropertyDescriptors() 与 Object.defineProperties() 或 Object.create() 结合使用,它可以帮助我们处理两个用例。
9.6.1 用例:将属性复制到对象中
自 ES6 以来,JavaScript 已经有一个用于复制属性的工具方法:Object.assign()。但是,此方法使用简单的 get 和 set 操作来复制键为 key 的属性
target[key] = source[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,
});
9.6.1.1 陷阱:复制使用 super 的方法
使用 super 的方法与其*宿主对象*(存储它的对象)紧密相连。目前没有办法将此类方法复制或移动到不同的对象。
9.6.2 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 “复制对象和数组”。
9.7 省略描述符属性
描述符的所有属性都是可选的。省略属性时会发生什么取决于操作。
9.7.1 创建属性时省略描述符属性
当我们通过描述符创建新属性时,省略属性意味着将使用它们的默认值
const car = {};
Object.defineProperty(
car, 'color', {
value: 'red',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'red',
writable: false,
enumerable: false,
configurable: false,
});
9.7.2 更改属性时省略描述符属性
相反,如果我们更改现有属性,则省略描述符属性意味着不会触及相应的属性
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,
});
9.8 内置构造函数使用哪些属性属性?
属性属性的一般规则(几乎没有例外)是
原型链开头处的对象的属性通常是可写的、可枚举的和可配置的。
正如关于可枚举性的章节中所述,大多数继承的属性都是不可枚举的,以便将它们从 for-in 循环等遗留结构中隐藏起来。继承的属性通常是可写的和可配置的。
9.8.1 通过赋值创建的自有属性
const obj = {};
obj.prop = 3;
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
}
});
9.8.2 通过对象字面量创建的自有属性
const obj = { prop: 'yes' };
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 'yes',
writable: true,
enumerable: true,
configurable: true
}
});
9.8.3 数组的自有属性 .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 是一种特殊的数据属性,因为它受其他自有属性(特别是索引属性)的影响(并且也会影响其他自有属性)。
9.8.4 内置类的原型属性
assert.deepEqual(
Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
{
value: Array.prototype.map,
writable: true,
enumerable: false,
configurable: true
});
9.8.5 用户定义类的原型属性和实例属性
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,
}
});
9.9 API:属性描述符
以下工具方法使用属性描述符:
Object.defineProperty(obj: object, key: string|symbol, propDesc: PropertyDescriptor): object [ES5]
在 obj 上创建或更改一个属性,其键为 key,其属性通过 propDesc 指定。返回修改后的对象。
const obj = {};
const result = Object.defineProperty(
obj, 'happy', {
value: 'yes',
writable: true,
enumerable: true,
configurable: true,
});
// obj was returned and modified:
assert.equal(result, obj);
assert.deepEqual(obj, {
happy: 'yes',
});
Object.defineProperties(obj: object, properties: {[k: string|symbol]: PropertyDescriptor}): object [ES5]
Object.defineProperty() 的批量版本。对象 properties 的每个属性 p 指定要添加到 obj 的一个属性:p 的键指定属性的键,p 的值是一个描述符,用于指定属性的特性。
const address1 = Object.defineProperties({}, {
street: { value: 'Evergreen Terrace', enumerable: true },
number: { value: 742, enumerable: true },
});
Object.create(proto: null|object, properties?: {[k: string|symbol]: PropertyDescriptor}): object [ES5]
首先,创建一个原型为 proto 的对象。然后,如果提供了可选参数 properties,则向其添加属性,方式与 Object.defineProperties() 相同。最后,返回结果。例如,以下代码片段与前一个代码片段产生相同的结果:
const address2 = Object.create(Object.prototype, {
street: { value: 'Evergreen Terrace', enumerable: true },
number: { value: 742, enumerable: true },
});
assert.deepEqual(address1, address2);
Object.getOwnPropertyDescriptor(obj: object, key: string|symbol): undefined|PropertyDescriptor [ES5]
返回 obj 的自有(非继承)属性的描述符,其键为 key。如果没有这样的属性,则返回 undefined。
assert.deepEqual(
Object.getOwnPropertyDescriptor(Object.prototype, 'toString'),
{
value: {}.toString,
writable: true,
enumerable: false,
configurable: true,
});
assert.equal(
Object.getOwnPropertyDescriptor({}, 'toString'),
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() 可以正常工作。
9.10 进一步阅读
接下来的三章将提供有关属性特性的更多详细信息:
§10 “保护对象不被更改”
§11 “属性:赋值与定义”
§12 “属性的可枚举性”
评论
下一页:10 保护对象不被更改