JavaScript物件的的建立及屬性狀態維護詳解

SimonZhang發表於2017-05-02

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

About

GitHub: github.com/SimonZhangI…
個人主頁: zhangr.top/

相關文章