(广告,请勿屏蔽。)

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 保护对象不被更改