在說屬性之前,我們先來了解一下ES5的新方法,Object.create()函數。
新的對象創建方法
在舊的“原型繼承”觀念中,它的本質上是“複製原型”,即:以原型爲模板複製一個新的對象。然而我們應該注意到一點事實:在這個思路上,“構造器函數”本身是無意義的。更確切的說,構造器函數對實例的修飾作用可有可無,例如:
//在構造器中修飾對象實例
function MyObject(){
this.yyy = ...;
}
當意識到這一點後,ES5實現Object.cerate()這樣一種簡單的方法,通過這一方法將“構造器函數”從對象創建過程中趕了出去。在新的機制中,對象變成了簡單的“原型繼承+屬性定義”,而不再需要“構造器”這樣一層語義,例如:
//新的對象創建方法
newObj = Object.create(prototypeObj,PropertyDescriptors);
這裏的PropertyDescriptors是一組屬性描述符,用於聲明基於prototypeObj這個原型之上的一些新的屬性添加或修改,它與defineProperties()方法中的props參數是一樣的,並在事實上也將調用後者。它的用法如下例所示:
var aPrototypeObject = {name1:"value1"};
var aNewInstance = Object.create(aPrototypeObject,{
name2:{value:'value2'},
name3:{get:function(){ return 'value3' }}
})
很顯然,在這種新方案中我們看不到類似MyObject()那樣的構造器了。事實上在引擎實現Object.create()時也並不特別地聲明某個構造器。
所以,所有由Object.create()創建的對象實例具有各自不同的原型(這取決於調用create()方法時傳入的參數),但它們的constractor值指向相同的引用——引擎內建的Object構造器。
屬性狀態維護
ES5中在Object()上聲明瞭三組方法,用於維護對象本身在屬性方面的信息,如下表(Markdown不會使用分組列表,大家湊合看看。。如果有知道的也告訴我一下哈~)
分類 | 方法 | 說明 |
---|---|---|
取屬性列表 | getOwnPropertyNames(obj) | 取對象自有的屬性名數組 |
取屬性列表 | keys(obj) | 取對象自由的、可見的屬性名數組 |
狀態維護 | preventExtensions(obj) | 使實例obj不能添加新屬性 |
狀態維護 | seal(obj) | 使實例obj不能添加新屬性,也不能刪除既有屬性 |
狀態維護 | freeze(obj) | 使實例obj所有屬性只讀,且不能再添加、刪除屬性 |
狀態檢查 | isExtensible(obj) | 返回preventExtensions狀態 |
狀態檢查 | isSealed(obj) | 返回seal狀態 |
狀態檢查 | isFrozen(obj) | 返回freeze狀態 |
其中,preventExtensions、seal和freeze三種狀態都是針對對象來操作的,會影響到所有屬性的性質的設置。需要強調的有兩點:
- 由原型繼承來的性質同樣會受到影響
- 以當前對象作爲原型時,子類可以通過重新定義同名屬性來覆蓋這些狀態
更進一步的說,這三種狀態是無法影響子類使用defineProperty()和defineProperties()來“重新定義(覆蓋)”同名屬性的。
本質上說,delete運算是用於刪除運算對象屬性的屬性描述符,而非某個屬性。
取屬性列表
取屬性列表的傳統方法是使用for…in語句。爲方便後續討論,我們先爲該語句封裝一個與Object.keys()類似的方法:
Object.forIn = function(obj){
var Result = [];
for(var n in obj) Result.push(n);
return Result;
}
forIn()得到的總是該對象全部可見的屬性列表。而keys()將是其中的一個子集,即“自有的(不包括繼承而來的)”可見屬性列表。下面的例子將顯示二者的不同:
var obj1 = {n1:100};
var obj2 = Object.create(obj1,{n2 : {value :200,enumerable:true}});
//顯示'n1' , 'n2'
// - 其中n1繼承自obj1
alert(Object.forIn(obj2));
//顯示'n2'
alert(Object.keys(obj2));
getOwnPropertyNames()得到的與上述兩種情況都不相同。它列舉全部自有的屬性,但無論它是否可見。也就是說,它是keys()所列舉內容的超集,包括全部可見和不可見的、自有的屬性。仍以上述爲例:
// (續上例)
//定義屬性名n3,其enumerable性質默認爲false
Object.defineProperty(obj2,'n3',{value:300})
//仍然顯示'n1','n2'
// - 新定義的n3不可見
alert(Object.forIn(obj2));
//顯示'n2'
alert(Object.keys(obj2));
//顯示n2,n3
alert(Object.getOwnPropertyNames(obj2));
使用defineProperty來維護屬性的性質
在defineProperty()或defineProperties()中操作某個屬性時,如果該名字的屬性未聲明則新建它;如果已經存在,則使用描述符中的新的性質來覆蓋舊的性質值。
這也意味着一個使用”數據屬性描述符”的屬性,也可以重新使用”存取屬性描述符”——但總的來說只能存在其中一個。例如:
var pOld,pNew;
var obj = { data : 'oldValue'}
//顯示'value,writable,enumerable,configuable'
pOld = Object.getOwnPropertyDescriptor(obj,'data');
alert(Object.keys(pOld));
//步驟一:通過一個閉包來保存舊的obj.data的值
Object.defineProperty(obj,'data',function(oldValue){
return {
get:function(){ return oldValue},
configurable:false
}
}(obj.data))
//顯示'get,set,enumerable,configurable'
pNew = Object.getOwnPropertyDescriptor(obj,'data');
alert(pNew);
//步驟二:測試使用重定義的getter來取obj.data的值
// - 顯示 'oldValue'
alert(obj.data);
//步驟三:(測試)嘗試再次聲明data屬性
// - 由於在步驟一中已經設置configurable爲false,因此導致異常(can't redefine)。
Object.defineProperty(obj,'data',{value:100});
對於繼承自原型的屬性,修改其值的效果
如果某個從原型繼承來的屬性是可寫的,並且它使用的是”數據屬性描述符”,那麼在子類中修改該值,將隱式地創建一個屬性描述符。這個新屬性描述符將按照”向對象添加一個屬性”的規格來初始化。即:必然是數據屬性描述符,且Writable,Enumerable和Configurable均爲true值。例如:
var obj1 = {n1 : 100};
var obj2 = Object.create(obj1);
//顯示爲空
// - 重置n1的enumerable性質爲false,因此在obj1中是不可見的
Object.defineProperty(obj1,'n1',{enumerable:false})
alert(Object.keys(obj1));
//顯示爲空
// - n1不是obj2的自有屬性
alert(Object.getOwnPropertyNames(obj2));
//顯示n1
// - 由於n1賦值導致新的屬性描述符,因此n1成爲了自有的屬性
obj2.n1 = 'newValue';
alert(Object.getOwnPropertyNames(obj2));
//顯示n1,表明n1是可見的
// - 由於新的屬性描述符的enumerable重置爲true,因此在obj2中它是可見的
alert(Object.keys(obj2));
如果一個屬性使用的是”存取屬性描述符”,那麼無論它的讀寫性爲何,都不會新建屬性描述符。對子類中該屬性的讀寫,都只會忠誠地調用(繼承而來的、原型中的)讀寫器。
重寫原型繼承來的屬性的描述符
使用defineProperty()或defineProperties()將重新定義該屬性,會顯式的創建一個屬性描述符。在這種情況下,該屬性也將變成自雷對象中”自有的”屬性,它的可見性等性質就由新的描述符來決定。
與上一小節不同的是,這與原型中該屬性是否”只讀”或是否允許修改性質(configurable)無關。
這可能導致類似如下的情況:在父類中某個屬性時只讀的,並且不可修改其描述符性質的,但是在子類中,同一個名字的屬性卻可以讀寫並可以重新修改性質。更爲嚴重的是,僅僅觀察兩個對象實例的外觀,我們無法識別這種差異是如何導致的。下面的示例說明這種情況:
var obj1 = {n1 : 100};
var obj2 = Object.create(obj1);
//對於原型對象obj1,修改其屬性n1的性質,使其不可列舉、修改、且不能重設性質
Object.defineProperty(obj1,'n1',{writable:false,enumerable:false,configurable:false});
//顯示爲空,obj1.n1是不可列舉的
alert(Object.keys(obj1));
//由於不可重設性質,因此對obj1.n1的下述調用將導致異常
//Object.defineProperty(obj1,'n1',{configurable:true});
接下來我們觀察”重新定義屬性”帶來的效果:
//(續上例)
//重新定義obj2.n1
Object.defineProperty(obj2,'n1',{value:obj2.n1,writable:true,enumerable:true,configurable:true});
//顯示newValue'
// - 結論:可以通過重定義屬性,使該屬性從"只讀"變成"可讀寫"(以及其他性質的變化)
obj2.n1 = 'newValue';
alert(obj2.n1);
//列舉obj2的自有性質,結果顯示:n1
// - 現在n1是自有的屬性了
alert(Object.getOwnpropertyNames(obj2));
從表面上看,一個父類中只讀的屬性在子類變成了可讀寫。而且,一旦我們用delete刪除該屬性,它又會恢復父類中的值和性質。例如:
//嘗試刪除該屬性
// - 顯示100,即它在原型中的值
delete obj2.n1;
alert(obj2.n1);
再次強調這一事實:在ES5中沒有任何方法可以阻止上述過程。也就是說,我們無法阻止子類對父類同名屬性的重定義,也無法避免這種重定義可能帶來的業務邏輯問題。