=
永遠使用指定有兩種方法可以建立或變更物件 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`。
以下是演算法的高階摘要
更詳細來說,此演算法的運作方式如下
如果 `O` 有原型 `parent`,則我們會傳回 `parent.[[Set]](P, V, Receiver)`。這會繼續我們的搜尋。方法呼叫通常會結束於遞迴呼叫 `OrdinarySetWithOwnDescriptor()`。
否則,我們搜尋 `P` 的動作已失敗,我們會設定 `ownDesc` 如下
{
[[Value]]: undefined, [[Writable]]: true,
[[Enumerable]]: true, [[Configurable]]: true
}
有了這個 `ownDesc`,下一個 `if` 陳述式會在 `Receiver` 中建立一個自有屬性。
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
是屬性描述符,指定我們希望屬性具有的屬性。current
包含自己的屬性 O[P]
的屬性描述符 (如果存在)。否則,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');
「原型鏈」一節,出自「JavaScript for impatient programmers」
Allen Wirfs-Brock 發送給 es-discuss 郵件清單的電子郵件:「指定和定義之間的區別[…]在 ES 只有資料屬性且 ES 程式碼無法處理屬性屬性的時候並不重要。」[這在 ECMAScript 5 中有所改變。]