原文:http://www.2ality.com/2012/10/javascript-properties.html
在JavaScript中,屬性決定了一個對象的狀態,本文詳細的研究了它們是如何工作的.
屬性類型
JavaScript中有三種不同類型的屬性:命名數據屬性(named data properties),命名訪問器屬性(named accessor properties)以及內部屬性(internal properties).
命名數據屬性
這種屬性就是我們通常所用的"普通"屬性,它用來將一個字符串名稱映射到某個值上.比如,下面的對象obj有一個名為字符串"prop"的數據屬性,該屬性的值為數字123.
var obj = {
prop: 123 };
你可以獲取(讀取)到一個屬性的值:
console.log(obj.prop); // 123 console.log(obj["prop"]); // 123
你還可以設置(寫入)一個屬性的值:
obj.prop = "abc";
obj["prop"] = "abc";
命名訪問器屬性
另外,還可以借助函數來獲取或設置一個屬性的值.這些函數稱之為訪問器函數(accessor function).控制屬性讀取的訪問器函數稱之為getter.控制屬性寫入的訪問器函數稱之為setter.
var obj = {
get prop() {
return "Getter";
},
set prop(value) {
console.log("Setter: "+value);
}
}
讓我們操作一下obj的屬性:
> obj.prop
'Getter'
> obj.prop = 123;
Setter: 123
內部屬性
有一些屬性僅僅是為規范所用的,稱之為內部屬性,因為它們無法通過JavaScript直接訪問到,但是它們的確存在,並且影響着程序的表現.內部屬性的名稱比較特殊,它們都被兩個中括號包圍着.下面有兩個例子:
- 內部屬性[[Prototype]]指向了所屬對象的原型.該屬性的值可以通過Object.getPrototypeOf()函數讀取到.該屬性的值只能在創建一個新對象的時候通過Object.create()或者__proto__來設置 [1].
- 內部屬性[[Extensible]]決定了是否能給所屬對象添加新的屬性.該屬性的值可以通過Object.isExtensible()讀取到.還可以通過Object.preventExtensions()將該屬性的值設置為false.一旦設置為false,就無法再設置回true了.
屬性特性
一個屬性的所有狀態,包括它的數據和元數據,都存儲在該屬性的特性(attributes)中.屬性擁有自己的特性,就像對象擁有自己的屬性一樣.特性的名稱經常寫成類似內部屬性的形式(雙中括號).
下面是命名數據屬性擁有的特性:
- [[Value]] 存儲着屬性的值,也就是屬性的數據.
- [[Writable]] 存儲着一個布爾值,表明該屬性的值是否可以改變.
下面是命名訪問器屬性擁有的特性:
- [[Get]] 存儲着getter,也就是在讀取這個屬性時調用的函數.該函數返回的值也就是這個屬性的值.
- [[Set]] 存儲着setter,也就是在為這個屬性賦值時調用的函數.該函數在調用時會被傳入一個參數,參數的值為所賦的那個新值.
下面是兩種類型的屬性都有的特性:
- [[Enumerable]] 存儲着一個布爾值.可以讓一個屬性不能被枚舉,在某些操作下隱藏掉自己(下面會有詳細講解).
- [[Configurable]] 存儲着一個布爾值.如果為false,則你不能刪除這個屬性,不能改變這個屬性的大部分特性(除了[[Value]]),不能將一個數據屬性重定義成訪問器屬性,或者反之.換句話說就是:[[Configurable]]控制了一個屬性的元數據的可寫性.
默認值
如果你不明確的指定某個特性的值,則它們會被賦一個默認值:
特性名稱 | 默認值 |
[[Value]] | undefined |
[[Get]] | undefined |
[[Set]] | undefined |
[[Writable]] | false |
[[Enumerable]] | false |
[[Configurable]] | false |
這些默認值對於屬性描述符尤其重要.
屬性描述符
屬性描述符(property descriptor)可以將一個屬性的所有特性編碼成一個對象並返回.該對象的每個屬性都對應着所屬屬性的一個特性.例如,下面是一個值為123的只讀屬性的屬性描述符:
{
value: 123,
writable: false,
enumerable: true,
configurable: false }
你也可以使用一個訪問器屬性來實現上面這個擁有只讀特性的數據屬性,其屬性描述符如下:
{ get: function () { return 123 }, //沒有set,也就是只讀 enumerable: true, configurable: false }
使用屬性描述符的函數
在使用下面的函數時會用到屬性描述符:
- Object.defineProperty(obj, propName, propDesc)
創建或改變對象obj的propName屬性,propName屬性的特性通過屬性描述符propDesc給出.返回修改后的obj對象.例如:var obj = Object.defineProperty({}, "foo", {
value: 123,
enumerable: true // writable和configurable為默認值 }); - Object.defineProperties(obj, propDescObj)
Object.defineProperty()的批處理版本.對象propDescObj的每個屬性都指定了要給原對象obj添加或修改的一個屬性和對應的屬性描述符.例如:var obj = Object.definePropertys({}, {
foo: { value: 123, enumerable: true },
bar: { value: "abc", enumerable: true }
}); - Object.create(proto, propDescObj?)
首先,創建一個原型為proto的對象.然后,如果提供了可選參數propDescObj,則會按照Object.defineProperties添加屬性的方式給這個新對象添加屬性.最后,返回操作后的新對象.例如,下面的代碼創建的對象和上面的Object.definePropertys例子創建的對象完全一樣:var obj = Object.create(Object.prototype, {
foo: { value: 123, enumerable: true },
bar: { value: "abc", enumerable: true }
}); - Object.getOwnPropertyDescriptor(obj, propName)
返回對象obj的名為propName的自身屬性(非繼承來的)的屬性描述符.如果沒有這個自身屬性,則返回undefined.> Object.getOwnPropertyDescriptor(Object.prototype, "toString")
{ value: [Function: toString],
writable: true,
enumerable: false,
configurable: true }
> Object.getOwnPropertyDescriptor({}, "toString")
undefined
可枚舉性
本節會解釋什么操作會受到屬性的可枚舉性的影響,什么操作不會.我們首先假設已經定義了如下這樣的對象proto和obj:
var proto = Object.defineProperties({}, {
foo: { value: 1, enumerable: true },
bar: { value: 2, enumerable: false }
});
var obj = Object.create(proto, {
baz: { value: 1, enumerable: true },
qux: { value: 2, enumerable: false }
});
需要注意的是,所有對象(包括上面的proto)通常來說都至少有一個原型Object.prototype [2]:
> Object.getPrototypeOf({}) === Object.prototype
true
我們常用的內置方法比如toString和hasOwnPropertyare等實際上都是定義在Object.prototype身上的.
受可枚舉性影響的操作
可枚舉性只影響兩種操作:for-in循環和Object.keys().
for-in循環會遍歷到一個對象的所有可枚舉屬性的名稱,包括繼承來的屬性:
> for (var x in obj) console.log(x); //沒有遍歷到Object.prototype上不可枚舉的屬性qux baz foo
Object.keys()返回一個對象的所有可枚舉的自身屬性(非繼承的)的名稱組成的數組:
> Object.keys(obj)
[ 'baz' ]
如果你想獲取到所有的自身屬性,則應該使用Object.getOwnPropertyNames().
不受可枚舉性影響的操作
除了上面的兩個操作,其他的操作都會忽略掉屬性的可枚舉性.一些讀取操作會使用到繼承來的屬性:
> "toString" in obj
true > obj.toString
[Function: toString]
還有一些操作只會考慮自身屬性:
> Object.getOwnPropertyNames(obj)
[ 'baz', 'qux' ]
> obj.hasOwnProperty("qux")
true > obj.hasOwnProperty("toString")
false > Object.getOwnPropertyDescriptor(obj, "qux")
{ value: 2,
writable: false,
enumerable: false,
configurable: false }
> Object.getOwnPropertyDescriptor(obj, "toString")
undefined
創建,刪除,定義屬性的操作只會影響到自身屬性:
obj.propName = value
obj["propName"] = value
delete obj.propName
delete obj["propName"]
Object.defineProperty(obj, propName, desc)
Object.defineProperties(obj, descObj)
最佳實踐
一般的規則是:系統創建的屬性是不可枚舉的,用戶創建的屬性是可枚舉的:
> Object.keys([])
[]
> Object.getOwnPropertyNames([])
[ 'length' ]
> Object.keys(['a'])
[ '0' ]
特別是針對原型對象上的方法來說:
> Object.keys(Object.prototype)
[]
> Object.getOwnPropertyNames(Object.prototype)
[ hasOwnProperty',
'valueOf',
'constructor',
'toLocaleString',
'isPrototypeOf',
'propertyIsEnumerable',
'toString' ]
因此,在你自己寫的代碼中,通常不應該給內置的原型對象添加屬性,如果你必須要這么做,則應該把這個屬性設置為不可枚舉的,以防止影響到其他代碼.
正如我們所看到的,不可枚舉的好處是:能確保已有的代碼中的for-in語句不受到從原型繼承來的屬性的影響.但是,不可枚舉的屬性只能夠創建一種"for-in只會遍歷一個對象的自身屬性"這樣的幻覺.在你的代碼中,仍應該盡可能避免使用for-in[3].
如果你把對象當成是字符串到值的Map來使用的話,則你應該只操作自身屬性且要忽略掉可枚舉性.不過這種情況下還有很多其他陷阱需要考慮[4].
結論
在本文中,我們對屬性的性質(稱之為特性)進行了研究.需要注意的是,實際上JavaScript引擎並不是必須得通過特性來組織一個屬性,它們主要是作為ECMAScript規范中定義的一個抽象操作.但有時候這些特性也會明確的出現在語言代碼中,比如在屬性描述符中.
更進一步的知識(2ality):
- 閱讀 “JavaScript properties: inheritance and enumerability” 了解更多的受繼承和枚舉性影響的屬性相關操作的知識.
- 閱讀 “JavaScript inheritance by example” 進一步了解JavaScript的繼承.