ES5對象的的創建及屬性狀態維護分析

在說屬性之前,我們先來了解一下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中沒有任何方法可以阻止上述過程。也就是說,我們無法阻止子類對父類同名屬性的重定義,也無法避免這種重定義可能帶來的業務邏輯問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章