原文:http://www.2ality.com/2012/08/property-definition-assignment.html
你知道嗎?定義一個屬性和為一個屬性賦值是有區別的.本文解釋了兩者之間的區別以及各自的作用影響.該話題來自於Allen Wirfs-Brock在es-discuss郵件列表中的一封郵件.
1. 定義VS賦值
定義(Definition).定義屬性需要使用相應的函數,比如:
Object.defineProperty(obj, "prop", propDesc)
如果obj沒有prop這個自身屬性,則該函數的作用是給obj添加一個自身屬性prop並賦值,參數propDesc指定了該屬性擁有的特性(可寫性,可枚舉性等).如果obj已經有了prop這個自身屬性,則該函數的作用是修改這個已有屬性的特性,當然也包括它的屬性值.
賦值(assignment).為一個屬性賦值需要使用下面的表達式:
obj.prop = value
如果obj已經有了prop這個自身屬性,則該表達式的作用就是修改這個prop屬性的值,反之,如果obj沒有prop這個自身屬性,則該表達式的操作結果就不一定了:首先會查找對象obj的原型鏈[1],如果原型鏈中的所有對象都沒有名為prop的屬性,則結果是在obj身上新建一個自身屬性prop,新創建的屬性擁有默認的屬性特性,且把指定的value賦值給該屬性.但如果obj的某個上層原型中上有一個名為prop的屬性,那接下來的操作就復雜了,具體請看下面的3.2小節.
2. 屬性特性和內部屬性
2.1. 多種類型的屬性
JavaScript中有三種類型的屬性:
- 命名數據屬性(named data properties): 擁有一個確定的值的屬性.這也是最常用的屬性.
- 命名訪問器屬性(named accessor properties): 通過getter和setter進行讀取和賦值的屬性.
- 內部屬性(internal properties): 由JavaScript引擎內部使用的屬性,不能通過JavaScript代碼直接訪問到,不過可以通過一些方法間接的讀取和設置.比如:每個對象都有一個內部屬性[[Prototype]],你不能直接訪問這個屬性,但可以通過Object.getPrototypeOf()方法間接的讀取到它的值.雖然內部屬性通常用一個雙中括號包圍的名稱來表示,但實際上這並不是它們的名字,它們是一種抽象操作,是不可見的,根本沒有上面兩種屬性有的那種字符串類型的屬性名.
2.2. 屬性特性
每個屬性(property)都擁有4個特性(attribute).兩種類型的屬性一種有6種屬性特性:
- 命名數據屬性特有的特性:
- [[Value]]: 屬性的值.
- [[Writable]]: 控制屬性的值是否可以改變.
- 命名訪問器屬性特有的特性:
- [[Get]]: 存儲着getter方法.
- [[Set]]: 存儲着setter方法.
- 兩種屬性都有的特性:
- [[Enumerable]]: 如果一個屬性是不可枚舉的,則在一些操作下,這個屬性是不可見的,比如for...in和Object.keys()[2].
- [[Configurable]]: 如果一個屬性是不可配置的,則該屬性的所有特性(除了[[Value]])都不可改變.
2.3. 屬性描述符
屬性描述符(property descriptor)可以將一個屬性的所有特性編碼成一個對象並返回.例如:
{
value: 123,
writable: false }
屬性描述符使用在下面的這些函數中:Object.defineProperty, Object.getOwnPropertyDescriptor, Object.create.如果省略了屬性描述符對象中的某個屬性,則該屬性會取一個默認值:
屬性名 | 默認值 |
value | undefined |
get | undefined |
set | undefined |
writable | false |
enumerable | false |
configurable | false |
2.4. 內部屬性
下面列舉幾個所有對象都有的內部屬性:
- [[Prototype]]: 對象的原型.
- [[Extensible]]: 對象是否可以擴展,也就是是否可以添加新的屬性.
- [[DefineOwnProperty]]: 定義一個屬性的內部方法.下一節會詳細解釋.
- [[Put]]: 為一個屬性賦值的內部方法.下一節會詳細解釋.
3. 屬性定義和屬性賦值
3.1. 屬性定義
定義屬性是通過內部方法來進行操作的:
[[ DefineOwnProperty]] (P, Desc, Throw)
P是要定義的屬性名稱.參數Throw決定了在定義操作被拒絕的時候是否要拋出異常:如果Throw為true,則拋出異常.否則,操作只會靜默失敗.當調用[[DefineOwnProperty]]時,具體會執行下面的操作步驟.
- 如果this沒有名為P的自身屬性的話:如果this是可擴展的話,則創建P這個自身屬性,否則拒絕.
- 如果this已經有了名為P的自身屬性:則按照下面的步驟重新配置這個屬性.
- 如果這個已有的屬性是不可配置的,則進行下面的操作會被拒絕:
- 將一個數據屬性轉換成訪問器屬性,反之亦然
- 改變[[Configurable]]或[[Enumerable]]
- 該變[[Writable]]
- 在[[Writable]]為false時改變[[Value]]
- 改變[[Get]]或[[Set]]
- 否則,這個已有的屬性可以被重新配置.
如果Desc就是P屬性當前的屬性描述符,則該定義操作永遠不會被拒絕.
定義屬性的函數有兩個:Object.defineProperty和Object.defineProperties.例如:
Object.defineProperty(obj, propName, desc)
在引擎內部,會轉換成這樣的方法調用:
obj.[[DefineOwnProperty]](propName, desc, true)
3.2. 屬性賦值
為屬性賦值是通過內部方法進行操作的:
[[ Put]] (P, Value, Throw)
參數P以及Throw和[[DefineOwnProperty]]方法中的參數表現的一樣.在調用[[Put]]方法的時候,會執行下面這樣的操作步驟.
- 如果在原型鏈上存在一個名為P的只讀屬性(只讀的數據屬性或者沒有setter的訪問器屬性),則拒絕.
- 如果在原型鏈上存在一個名為P的且擁有setter的訪問器屬性:則調用這個setter.
- 如果沒有名為P的自身屬性:則如果這個對象是可擴展的,就使用下面的操作創建一個新屬性:
this.[[DefineOwnProperty]](
P,
{
value: Value,
writable: true,
enumerable: true,
configurable: true }, Throw ) - 否則, 如果已經存在一個可寫的名為P的自身屬性.則調用:
this.[[DefineOwnProperty]](P, { value: Value }, Throw)
賦值運算符(=)就是在調用[[Put]].比如:
obj.prop = v;
在引擎內部,會轉換成這樣的方法調用:
obj.[[Put]]("prop", v, isStrictModeOn)
isStrictModeOn對應着參數Throw.也就是說,賦值運算符只有在嚴格模式下才有可能拋出異常.[[Put]]沒有返回值,但賦值運算符有.
4. 作用及影響
本節講一下屬性的定義操作和賦值操作各自的作用及影響.
4.1. 賦值可能會調用原型上的setter,定義會創建一個自身屬性
給定一個空對象obj,他的原型proto有一個名為foo的訪問器屬性.
var proto = {
get foo() {
console.log("Getter");
return "a";
},
set foo(x) {
console.log("Setter: "+x);
},
};
var obj = Object.create(proto);
那么,"在obj身上定義一個foo屬性"和"為obj的foo屬性賦值"有什么區別呢?
如果是定義操作的話,則會在obj身上添加一個自身屬性foo:
> Object.defineProperty(obj, "foo", { value: "b" });
> obj.foo
'b'
> proto.foo
Getter
'a'
但如果為foo屬性賦值的話,則意味着你是想改變某個已經存在的屬性的值.所以這次賦值操作會轉交給原型proto的foo屬性的setter訪問器來處理,下面代碼的執行結果就能證明這一結論:
> obj.foo = "b";
Setter: b
'b'
你還可以定義一個只讀的訪問器屬性,辦法是:只定義一個getter,省略setter.下面的例子中,proto2的bar屬性就是這樣的只讀屬性,obj2繼承了這個屬性.
"use strict";
var proto2 = {
get bar() {
console.log("Getter");
return "a";
},
};
var obj2 = Object.create(proto2);
開啟嚴格模式的話,下面的賦值操作會拋出異常.非嚴格模式的話,賦值操作只會靜默失敗(不起任何作用,也不報錯).
> obj2.bar = "b";
TypeError: obj.bar is read-only
我們可以定義一個自身屬性bar,遮蔽從原型上繼承的bar屬性:
> Object.defineProperty(obj2, "bar", { value: "b" });
> obj2.bar
'b'
> proto2.bar
Getter
'a'
4.2. 原型鏈中的同名只讀屬性可能會阻止賦值操作,但不會阻止定義操作
如果原型鏈中存在一個同名的只讀屬性,則無法通過賦值的方式在原對象上添加這個自身屬性,必須使用定義操作才可以.這項限制是在ECMAScript 5.1中引入的:
"use strict";
var proto = Object.defineProperties(
{},
{
foo: { // foo屬性的特性: value: "a",
writable: false, // 只讀 configurable: true // 可配置 }
});
var obj = Object.create(proto);
賦值.賦值操作會導致異常:
> obj.foo = "b"; //譯者注:貌似只有Firefox遵循了這個標准 TypeError: obj.foo is read-only
這貌似是個很奇怪的表現,原型上的屬性居然可以影響到能否創建一個同名的自身屬性 [3].但是這樣的表現是有道理的,因為另外一種形式的只讀屬性(只有getter的訪問器屬性)也是這樣的表現,這樣才能統一.
定義.通過定義的方式,我們可以成功創建一個新的自身屬性:
> Object.defineProperty(obj, "foo", { value: "b" });
> obj.foo
'b'
> proto.foo
'a'
4.3. 賦值運算符不會改變原型鏈上的屬性
執行下面的代碼,則obj會從proto上繼承到foo屬性.
var proto = { foo: "a" };
var obj = Object.create(proto);
你不能通過為obj.foo賦值來改變proto.foo的值.這種操作只會在obj上新建一個自身屬性:
> obj.foo = "b";
'b'
> obj.foo
'b'
> proto.foo
'a'
4.4. 只有通過定義操作,才能創建一個擁有指定特性的屬性
如果通過賦值操作創建了一個自身屬性,則該屬性始終擁有默認的特性.如果你想指定某個特性的值,必須通過定義操作.
4.5. 對象字面量中的屬性是通過定義操作添加的
下面的代碼將變量obj指向了一個對象字面量:
var obj = {
foo: 123 };
這樣的語句在引擎內部,可能會被轉換成下面兩種操作方式中的一種.首先可能是賦值操作:
var obj = new Object();
obj.foo = 123;
其次,可能是個定義操作:
var obj = new Object();
Object.defineProperties(obj, {
foo: {
value: 123,
enumerable: true,
configurable: true,
writable: true } });
到底是哪種呢?正確答案是第二種,因為第二種操作方式能夠更好的表達出對象字面量的語義:創建新的屬性.Object.create接受一個屬性描述符作為第二個可選參數,也是這個原因.
4.6. 方法屬性
可以通過定義操作新建一個只讀的方法屬性:
"use strict";
function Stack() {
}
Object.defineProperties(Stack.prototype, {
push: {
writable: false,
configurable: true,
value: function (x) { /* ... */ }
}
});
目的是為了防止在實例身上發生意外的賦值操作:
> var s = new Stack();
> s.push = 5;
TypeError: s.push is read-only
不過,由於push是可配置的,所以我們仍可以通過定義操作來為實例添加一個自身的push方法.
> var s = new Stack();
> Object.defineProperty(s, "push",
{ value: function () { return "yes" }})
> s.push()
'yes'
我們甚至可以通過定義操作來重新定義原型上的push方法:Stack.prototype.push.
5. 結論
我們經常會通過為屬性賦值來給一個對象添加新的屬性.本文解釋了這樣做是可能引發一些問題的.因此,最好要遵循下面的規則:
- 如果你想創建一個新屬性,則使用屬性定義.
- 如果你想該變一個屬性的值,則使用屬性賦值.
在評論中,medikoo提醒了我們使用屬性描述符來創建屬性可能會有點慢.我就經常通過為屬性賦值來創建新屬性,因為這樣很方便.值得高興的是,ECMAScript.next也許會把屬性的定義操作變的既快又方便:已經存在一個“定義屬性的運算符”的提案,可以作為Object.defineProperties的替代用法.由於屬性定義和屬性賦值之間的差別既很微妙,又很重要,所以這種改進應該會很受歡迎.