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

11 属性:赋值与定义



有两种方法可以创建或更改对象 obj 的属性 prop

本章解释它们是如何工作的。

  必备知识:属性特性和属性描述符

对于本章,您应该熟悉属性特性和属性描述符。如果您不熟悉,请查看 §9 “属性特性:简介”

11.1 赋值与定义

11.1.1 赋值

我们使用赋值运算符 = 将值 value 赋给对象 obj 的属性 .prop

obj.prop = value

此运算符的工作方式取决于 .prop 的外观

也就是说,赋值的主要目的是进行更改。这就是它支持设置器的原因。

11.1.2 定义

要定义对象 obj 的键为 propKey 的属性,我们使用如下方法之类的操作

Object.defineProperty(obj, propKey, propDesc)

此方法的工作方式取决于属性的外观

也就是说,定义的主要目的是创建一个自身属性(即使存在继承的设置器,它也会被忽略)并更改属性特性。

11.2 理论上的赋值和定义(可选)

  ECMAScript 规范中的属性描述符

在规范操作中,属性描述符不是 JavaScript 对象,而是 记录,这是一种具有字段的规范内部数据结构。字段的键用双括号括起来。例如,Desc.[[Configurable]] 访问 Desc 的字段 .[[Configurable]]。这些记录在与外部世界交互时会在 JavaScript 对象之间进行转换。

11.2.1 为属性赋值

为属性赋值的实际工作由 ECMAScript 规范中的 以下操作 处理

OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc)

以下是参数

返回值是一个布尔值,指示操作是否成功。如本章 稍后所述,如果 OrdinarySetWithOwnDescriptor() 失败,则严格模式赋值会抛出 TypeError

以下是该算法的高级摘要

更详细地说,该算法的工作原理如下

11.2.1.1 我们如何从赋值到 OrdinarySetWithOwnDescriptor()

评估没有解构的赋值涉及以下步骤

值得注意的是,如果 .[[Set]]() 的结果为 false,则 PutValue() 会在严格模式下抛出 TypeError

11.2.2 定义属性

定义属性的实际工作由 ECMAScript 规范中的 以下操作 处理

ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current)

参数如下

操作的结果是一个布尔值,指示它是否成功。失败可能会有不同的后果。一些调用者会忽略结果。而另一些调用者,例如 Object.defineProperty(),如果结果为 false,则会抛出异常。

以下是该算法的摘要

11.3 实践中的定义和赋值

本节介绍属性定义和赋值工作方式的一些结果。

11.3.1 只有定义允许我们创建具有任意特性的属性

如果我们通过赋值创建自己的属性,它总是会创建特性 writableenumerableconfigurable 都为 true 的属性。

const obj = {};
obj.dataProp = 'abc';
assert.deepEqual(
  Object.getOwnPropertyDescriptor(obj, 'dataProp'),
  {
    value: 'abc',
    writable: true,
    enumerable: true,
    configurable: true,
  });

因此,如果我们想指定任意特性,我们必须使用定义。

虽然我们可以在对象字面量中创建 getter 和 setter,但我们不能稍后通过赋值添加它们。在这里,我们也需要定义。

11.3.2 赋值运算符不会更改原型中的属性

让我们考虑以下设置,其中 objproto 继承属性 prop

const proto = { prop: 'a' };
const obj = Object.create(proto);

我们不能通过赋值给 obj.prop 来(破坏性地)更改 proto.prop。这样做会创建一个新的自身属性

assert.deepEqual(
  Object.keys(obj), []);

obj.prop = 'b';

// The assignment worked:
assert.equal(obj.prop, 'b');

// But we created an own property and overrode proto.prop,
// we did not change it:
assert.deepEqual(
  Object.keys(obj), ['prop']);
assert.equal(proto.prop, 'a');

这种行为的基本原理如下:原型可以具有其所有后代共享其值的属性。如果我们只想在一个后代中更改此类属性,则必须通过覆盖来非破坏性地进行更改。然后,更改不会影响其他后代。

11.3.3 赋值调用 setter,定义不调用

定义 obj 的属性 .prop 与赋值给它之间有什么区别?

如果我们定义,那么我们的意图是创建或更改 obj 的自身(非继承)属性。因此,定义会忽略以下示例中 .prop 的继承 setter

let setterWasCalled = false;
const proto = {
  get prop() {
    return 'protoGetter';
  },
  set prop(x) {
    setterWasCalled = true;
  },
};
const obj = Object.create(proto);

assert.equal(obj.prop, 'protoGetter');

// Defining obj.prop:
Object.defineProperty(
  obj, 'prop', { value: 'objData' });
assert.equal(setterWasCalled, false);

// We have overridden the getter:
assert.equal(obj.prop, 'objData');

相反,如果我们赋值给 .prop,那么我们的意图通常是更改已经存在的东西,并且该更改应该由 setter 处理

let setterWasCalled = false;
const proto = {
  get prop() {
    return 'protoGetter';
  },
  set prop(x) {
    setterWasCalled = true;
  },
};
const obj = Object.create(proto);

assert.equal(obj.prop, 'protoGetter');

// Assigning to obj.prop:
obj.prop = 'objData';
assert.equal(setterWasCalled, true);

// The getter still active:
assert.equal(obj.prop, 'protoGetter');

11.3.4 继承的只读属性阻止通过赋值创建自身属性

如果 .prop 在原型中是只读的,会发生什么?

const proto = Object.defineProperty(
  {}, 'prop', {
    value: 'protoValue',
    writable: false,
  });

在从 proto 继承只读 .prop 的任何对象中,我们不能使用赋值来创建具有相同键的自身属性 - 例如

const obj = Object.create(proto);
assert.throws(
  () => obj.prop = 'objValue',
  /^TypeError: Cannot assign to read only property 'prop'/);

为什么我们不能赋值?基本原理是,通过创建自身属性来覆盖继承的属性可以看作是非破坏性地更改继承的属性。可以说,如果一个属性是不可写的,我们就不应该能够做到这一点。

但是,定义 .prop 仍然有效,并且允许我们覆盖

Object.defineProperty(
  obj, 'prop', { value: 'objValue' });
assert.equal(obj.prop, 'objValue');

没有 setter 的访问器属性也被认为是只读的

const proto = {
  get prop() {
    return 'protoValue';
  }
};
const obj = Object.create(proto);
assert.throws(
  () => obj.prop = 'objValue',
  /^TypeError: Cannot set property prop of #<Object> which has only a getter$/);

  “覆盖错误”:优缺点

只读属性阻止原型链中较早的赋值这一事实被称为*覆盖错误*

11.4 哪些语言结构使用定义,哪些使用赋值?

在本节中,我们将研究语言在哪里使用定义,在哪里使用赋值。我们通过跟踪是否调用了继承的 setter 来检测使用了哪种操作。有关更多信息,请参阅§11.3.3 “赋值调用 setter,定义不调用”

11.4.1 对象字面量的属性是通过定义添加的

当我们通过对象字面量创建属性时,JavaScript 总是使用定义(因此从不调用继承的 setter)

let lastSetterArgument;
const proto = {
  set prop(x) {
    lastSetterArgument = x;
  },
};
const obj = {
  __proto__: proto,
  prop: 'abc',
};
assert.equal(lastSetterArgument, undefined);

11.4.2 赋值运算符 = 始终使用赋值

赋值运算符 = 始终使用赋值来创建或更改属性。

let lastSetterArgument;
const proto = {
  set prop(x) {
    lastSetterArgument = x;
  },
};
const obj = Object.create(proto);

// Normal assignment:
obj.prop = 'abc';
assert.equal(lastSetterArgument, 'abc');

// Assigning via destructuring:
[obj.prop] = ['def'];
assert.equal(lastSetterArgument, 'def');

11.4.3 公共类字段是通过定义添加的

唉,即使公共类字段的语法与赋值相同,它们也*不*使用赋值来创建属性,而是使用定义(如对象字面量中的属性)

let lastSetterArgument1;
let lastSetterArgument2;
class A {
  set prop1(x) {
    lastSetterArgument1 = x;
  }
  set prop2(x) {
    lastSetterArgument2 = x;
  }
}
class B extends A {
  prop1 = 'one';
  constructor() {
    super();
    this.prop2 = 'two';
  }
}
new B();

// The public class field uses definition:
assert.equal(lastSetterArgument1, undefined);
// Inside the constructor, we trigger assignment:
assert.equal(lastSetterArgument2, 'two');

11.5 本章的进一步阅读和来源