=
始终使用赋值有两种方法可以创建或更改对象 obj
的属性 prop
obj.prop = true
Object.defineProperty(obj, '', {value: true})
本章解释它们是如何工作的。
必备知识:属性特性和属性描述符
对于本章,您应该熟悉属性特性和属性描述符。如果您不熟悉,请查看 §9 “属性特性:简介”。
我们使用赋值运算符 =
将值 value
赋给对象 obj
的属性 .prop
此运算符的工作方式取决于 .prop
的外观
更改属性:如果存在自身数据属性 .prop
,则赋值会将其值更改为 value
。
调用设置器:如果 .prop
存在自身或继承的设置器,则赋值会调用该设置器。
创建属性:如果不存在自身数据属性 .prop
并且也没有其自身或继承的设置器,则赋值会创建一个新的自身数据属性。
也就是说,赋值的主要目的是进行更改。这就是它支持设置器的原因。
要定义对象 obj
的键为 propKey
的属性,我们使用如下方法之类的操作
此方法的工作方式取决于属性的外观
propKey
的自身属性,则定义会根据属性描述符 propDesc
指定的内容更改其属性特性(如果可能)。propDesc
指定的属性的自身属性(如果可能)。也就是说,定义的主要目的是创建一个自身属性(即使存在继承的设置器,它也会被忽略)并更改属性特性。
ECMAScript 规范中的属性描述符
在规范操作中,属性描述符不是 JavaScript 对象,而是 记录,这是一种具有字段的规范内部数据结构。字段的键用双括号括起来。例如,Desc.[[Configurable]]
访问 Desc
的字段 .[[Configurable]]
。这些记录在与外部世界交互时会在 JavaScript 对象之间进行转换。
为属性赋值的实际工作由 ECMAScript 规范中的 以下操作 处理
OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc)
以下是参数
O
是当前正在访问的对象。P
是我们要赋值的属性的键。V
是我们要赋的值。Receiver
是赋值开始处的对象。ownDesc
是 O[P]
的描述符,如果该属性不存在,则为 null
。返回值是一个布尔值,指示操作是否成功。如本章 稍后所述,如果 OrdinarySetWithOwnDescriptor()
失败,则严格模式赋值会抛出 TypeError
。
以下是该算法的高级摘要
Receiver
的原型链,直到找到键为 P
的属性。遍历是通过递归调用 OrdinarySetWithOwnDescriptor()
完成的。在递归期间,O
会发生变化并指向当前正在访问的对象,但 Receiver
保持不变。Receiver
(递归开始处)中创建一个自身属性,或者发生其他情况。更详细地说,该算法的工作原理如下
ownDesc
为 undefined
,则我们尚未找到键为 P
的属性如果 O
有一个原型 parent
,则我们返回 parent.[[Set]](P, V, Receiver)
。这将继续我们的搜索。方法调用通常最终会递归调用 OrdinarySetWithOwnDescriptor()
。
否则,我们对 P
的搜索失败,我们按如下方式设置 ownDesc
{
[[Value]]: undefined, [[Writable]]: true,
[[Enumerable]]: true, [[Configurable]]: true
}
使用此 ownDesc
,下一个 if
语句将在 Receiver
中创建一个自身属性。
ownDesc
指定了一个数据属性,则我们找到了一个属性ownDesc.[[Writable]]
为 false
,则返回 false
。这意味着任何不可写的属性 P
(自身或继承的!)都会阻止赋值。existingDescriptor
为 Receiver.[[GetOwnProperty]](P)
。也就是说,检索赋值开始处属性的描述符。我们现在有O
和当前属性描述符 ownDesc
。Receiver
和原始属性描述符 existingDescriptor
。existingDescriptor
不为 undefined
Receiver
没有属性 P
时才递归。)if
条件永远不应该为 true
,因为 ownDesc
和 existingDesc
应该相等existingDescriptor
指定了一个访问器,则返回 false
。existingDescriptor.[[Writable]]
为 false
,则返回 false
。Receiver.[[DefineOwnProperty]](P, { [[Value]]: V })
。此内部方法执行定义,我们使用它来更改属性 Receiver[P]
的值。定义算法将在下一小节中介绍。Receiver
没有键为 P
的自身属性。)CreateDataProperty(Receiver, P, V)
。(此操作 在其第一个参数中创建一个自身数据属性。)ownDesc
描述了一个自身或继承的访问器属性。)setter
为 ownDesc.[[Set]]
。setter
为 undefined
,则返回 false
。Call(setter, Receiver, «V»)
。 Call()
调用函数对象 setter
,并将 this
设置为 Receiver
,并将单个参数设置为 V
(法语引号 «»
用于规范中的列表)。true
。OrdinarySetWithOwnDescriptor()
?评估没有解构的赋值涉及以下步骤
AssignmentExpression
运行时语义的部分 开始。本节介绍如何为匿名函数、解构等提供名称。PutValue()
进行赋值。PutValue()
调用内部方法 .[[Set]]()
。.[[Set]]()
调用 OrdinarySet()
(它调用 OrdinarySetWithOwnDescriptor()
)并返回结果。值得注意的是,如果 .[[Set]]()
的结果为 false
,则 PutValue()
会在严格模式下抛出 TypeError
。
定义属性的实际工作由 ECMAScript 规范中的 以下操作 处理
ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current)
参数如下
O
,我们要在其中定义属性。有一种特殊的仅验证模式,其中 O
为 undefined
。我们在这里忽略此模式。P
。extensible
指示 O
是否可扩展。Desc
是一个属性描述符,用于指定我们希望属性具有的特性。O[P]
,则 current
包含其属性描述符。否则,current
为 undefined
。操作的结果是一个布尔值,指示它是否成功。失败可能会有不同的后果。一些调用者会忽略结果。而另一些调用者,例如 Object.defineProperty()
,如果结果为 false
,则会抛出异常。
以下是该算法的摘要
如果 current
为 undefined
,则属性 P
当前不存在,必须创建。
extensible
为 false
,则返回 false
,表示无法添加该属性。Desc
并创建数据属性或访问器属性。true
。如果 Desc
没有任何字段,则返回 true
,表示操作成功(因为无需进行任何更改)。
如果 current.[[Configurable]]
为 false
Desc
更改 value
以外的属性。)Desc.[[Configurable]]
存在,则它必须与 current.[[Configurable]]
具有相同的值。如果不是,则返回 false
。Desc.[[Enumerable]]
接下来,我们验证属性描述符 Desc
:current
描述的属性是否可以更改为 Desc
指定的值?如果不能,则返回 false
。如果可以,则继续。
false
。.[[Configurable]]
和 .[[Enumerable]]
的值,所有其他属性都会获得 默认值(对于对象值属性为 undefined
,对于布尔值属性为 false
)。current.[[Configurable]]
和 current.[[Writable]]
都为 false
,则不允许进行任何更改,并且 Desc
和 current
必须指定相同的属性current.[[Configurable]]
为 false
,因此之前已经检查过 Desc.[[Configurable]]
和 Desc.[[Enumerable]]
并且它们的值正确。)Desc.[[Writable]]
存在且为 true
,则返回 false
。Desc.[[Value]]
存在且与 current.[[Value]]
的值不同,则返回 false
。true
表示算法成功。current.[[Configurable]]
为 false
,则不允许进行任何更改,并且 Desc
和 current
必须指定相同的特性current.[[Configurable]]
为 false
,因此之前已经检查过 Desc.[[Configurable]]
和 Desc.[[Enumerable]]
并且它们的值正确。)Desc.[[Set]]
存在,则它必须与 current.[[Set]]
具有相同的值。否则,返回 false
。Desc.[[Get]]
true
表示算法成功。将键 P
的属性的特性设置为 Desc
指定的值。由于验证,我们可以确保允许所有更改。
返回 true
。
本节介绍属性定义和赋值工作方式的一些结果。
如果我们通过赋值创建自己的属性,它总是会创建特性 writable
、enumerable
和 configurable
都为 true
的属性。
const obj = {};
obj.dataProp = 'abc';
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'dataProp'),
{
value: 'abc',
writable: true,
enumerable: true,
configurable: true,
});
因此,如果我们想指定任意特性,我们必须使用定义。
虽然我们可以在对象字面量中创建 getter 和 setter,但我们不能稍后通过赋值添加它们。在这里,我们也需要定义。
让我们考虑以下设置,其中 obj
从 proto
继承属性 prop
。
我们不能通过赋值给 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');
这种行为的基本原理如下:原型可以具有其所有后代共享其值的属性。如果我们只想在一个后代中更改此类属性,则必须通过覆盖来非破坏性地进行更改。然后,更改不会影响其他后代。
定义 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');
如果 .prop
在原型中是只读的,会发生什么?
在从 proto
继承只读 .prop
的任何对象中,我们不能使用赋值来创建具有相同键的自身属性 - 例如
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 'objValue',
/^TypeError: Cannot assign to read only property 'prop'/);
为什么我们不能赋值?基本原理是,通过创建自身属性来覆盖继承的属性可以看作是非破坏性地更改继承的属性。可以说,如果一个属性是不可写的,我们就不应该能够做到这一点。
但是,定义 .prop
仍然有效,并且允许我们覆盖
没有 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$/);
“覆盖错误”:优缺点
只读属性阻止原型链中较早的赋值这一事实被称为*覆盖错误*
在本节中,我们将研究语言在哪里使用定义,在哪里使用赋值。我们通过跟踪是否调用了继承的 setter 来检测使用了哪种操作。有关更多信息,请参阅§11.3.3 “赋值调用 setter,定义不调用”。
当我们通过对象字面量创建属性时,JavaScript 总是使用定义(因此从不调用继承的 setter)
let lastSetterArgument;
const proto = {
set prop(x) {
lastSetterArgument = x;
},
};
const obj = {
__proto__: proto,
prop: 'abc',
};
assert.equal(lastSetterArgument, undefined);
=
始终使用赋值赋值运算符 =
始终使用赋值来创建或更改属性。
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');
唉,即使公共类字段的语法与赋值相同,它们也*不*使用赋值来创建属性,而是使用定义(如对象字面量中的属性)
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');