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

9 属性属性:简介



在本章中,我们将仔细研究 ECMAScript 规范如何看待 JavaScript 对象。特别是,属性在规范中不是原子的,而是由多个*属性*(想想记录中的字段)组成的。即使是数据属性的值也存储在一个属性中!

9.1 对象的结构

在 ECMAScript 规范中,一个对象由以下部分组成

9.1.1 内部插槽

规范对内部插槽的描述如下。我添加了项目符号并强调了一部分

有两种内部插槽

普通对象具有以下数据插槽

9.1.2 属性键

属性的键可以是

9.1.3 属性属性

有两种属性,它们的特征在于它们的属性

此外,还有两种属性都具有的属性。下表列出了所有属性及其默认值。

属性类型 属性名称和类型 默认值
数据属性 value: any undefined
writable: boolean false
访问器属性 get: (this: any) => any undefined
set: (this: any, v: any) => void undefined
所有属性 configurable: boolean false
enumerable: boolean false

我们已经遇到了 valuegetset 属性。其他属性的工作原理如下

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 的属性,则会发生什么取决于

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];

这意味着它仅在以下情况下才会创建属性的忠实副本:

以下示例说明了此限制。对象 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 内置构造函数使用哪些属性属性?

属性属性的一般规则(几乎没有例外)是

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:属性描述符

以下工具方法使用属性描述符:

9.10 进一步阅读

接下来的三章将提供有关属性特性的更多详细信息: