尽管javascript里有大量内建引用对象,很可能你还说会频繁创建自己的对象。当你在这么做的时候,记得javascript中的对象时动态的,可在代码执行的任意时刻发生改变。基于类的语言会根据类的定义锁定对象,javascript对象没有这种限制。
javascript编程一大重点就是管理那些对象,这就是为什么理解对象如何运作时理解这个那个javascript的关键。本章后面会读词进行详细讨论。
3.1 定义属性
在第一章讲过,有两种创建自己的对象的方式:使用Object构造函数或使用对象的字面形式。例如:
var person1={
name:"Nicholas"
};
var person2=new Object();
person2.name="Nicholas";
person1.age ="Redacted";
person2.age="Redacted";
person1.name="Greg";
person2.name="Michael";
person1he person2都是具有name属性的对象。然后两个对象都被赋予age属性。你可以在对象定义之后立即给他添加属性,也可以等到后面。你创建的对象总是任你随意修改,除非你另外指定。例子最后改变了两个对象name的值,属性的值也可以被随时修改。
当一个属性第一次被添加给对象时,javascript在对象调用一个名为[[Put]]的内部方法。[[Put]]方法会在对象上创建一个新的节点来保存属性,就像第一次在哈希表上添加一个键一样。这个操作不仅指定了初始的值,也定义了属性的一些特征。所以在前例中,当属性name和age在每个对象上第一次被定义时,[[Put]]方法都在该对象上被调用啦。
调用[[Put]]的结果是在对象上创建一个自有属性。一个自有属性表明仅仅该指定的对象实例拥有该属性。该属性被直接保存在实例内,对该属性的所有操作都必须通过该对象进行。
注意:自有属性有别于原型属性
当一个已有属性被赋予新值时,调用的事一个名为[[Set]]的方法。该方法将属性的当前值替换为新值。上例为name设置第二个值时,调用了[[Set]]方法
3.2 屬性探測
由於屬性可以在任何時候添加,所以有時候就有必要檢查對象是否已有一個屬性。javascript開發新手常錯誤地使用以下模式探測屬性是否存在。
//unreliable
if(person1.age){
//do something with age
}
問題在於javascript的類型強制會影響該模式的輸出結果。當if判斷中的值是一個對象,非空字符串,非零數字或者是true時,判斷會評估爲真;而當值是一個null,undefined,0,false,NaN或空字符串時評估爲假。由於一個對象屬性可以包含這些假值,上例代碼有可能導致錯誤的假定。例如,當person1.age爲0時,雖然age屬性存在,if條件仍然無法滿足。更加可靠的判斷屬性是否存在的方法是使用in操作符。
in操作符在給定對象中查找一個給定名稱的屬性,如果找到則返回true。實際上in操作符就是在hash表中查找一個鍵是否存在。下例展示了用in檢查person1對象的某些屬性時會發生的情況。
console.log("name" in person1);
console.log("age" in person1);
console.log("title" in person);
方法就是值爲函數的屬性,所以你一樣可以檢查一個方法是否存在。下例給person1添加sayName()方法並用in來確認方法的存在。
var person1={
name:"Nicholas",
sayName:function(){
console.log(this.name);
}
}
在大多數情況下,in操作符是探測對象中屬性是否存在的最好的途徑。它還有一個額外的好處就是不會評估屬性的值。當此類評論會導致性能爲題或錯誤時,這一點就尤爲重要。
然而在某些情況下,你可能希望僅當一個屬性是自有屬性時才檢查是否存在。in操作符會檢查自有屬性和原型屬性,所以你不得不選擇另一條途徑:所有的對象都擁有的hasOwnProperty()方法,該方法在給定的屬性存在且爲自有屬性時返回true。下例代碼比較了in和hasOwnProperty()針對person1的不同屬性的結果。
var person1={
name:"Nicholas",
sayName:function(){
console.log(this.name);
}
};
console.log("name" in person1);
console.log(person1.hasOwnProperty("name"));
console.log("toString" in person1);
console.log(person1.hasOwnProperty("toString"));
在本例中,name時person1的一個自有屬性,所以in操作符和hasOwnProperty()都返回true。而toString()方法則是一個所有對象都具有的原型屬性。in操作符對toString返回true,而hasOwnProperty()怎返回false。這時個重大區別。在第四章會深入討論。
3.3 刪除屬性
正如屬性可以在任何時候被添加到對象上,它們也可以在任何時候被移除。但設置一個屬性爲null的值爲null並不能從對象中徹底移除那個屬性,只是調用[[Set]]將null值替換了該屬性原來的值而已,如果你不知道,就去讀前幾章,你需要使用delete操作符來徹底移除對象的一個屬性。
delete操作符針對單個對象屬性調用名爲[[Delete]]的內部方法。你可以認爲該操作在hash表中移除了一個鍵值對。當delete操作符成功時,它返回true。(某些屬性無法被移除,本章後續將詳細討論。)下例展示了delete操作符的用法。
var person1={
name:"Nicholas";
};
console.log("name" in person1); '// true
delete person1.name;
console.log("name" in person1); //false
console.log(person1.name); //undefined
本例中,name屬性被從person1中刪除。操作完後,in操作符返回false。試圖方法一個不存在的屬性將返回undefined。
3.4 屬性枚舉
所有你添加的屬性默認都是可枚舉的,也就是說你可以用for-in循環遍歷它們。可枚舉屬性的內部特徵[[Enumerable]]都被設置爲true。for-in循環會枚舉一個對象所有的可枚舉屬性並將屬性名賦給一個變量。下例循環將輸出object的屬性名字和值。
var property;
for(property in object){
console.log("Name:"+peroperty);
console.log("Value:"+object[property]);
}
for-in循環的每次迭代,object的下一個可枚舉屬性的名字就被賦給變量property,知道遍歷完所有可枚舉屬性。然後循環結束,代碼繼續執行。本例使用了中括號訪問對象屬性的值並輸出到控制檯。這是javascript的中括號的一個基本用法。
如果你只需要獲取一個對象的屬性列表以備程序將來使用,ECMAScript5引入了Object.keys()方法,它可以獲取可枚舉屬性的名字的數組,如下所示:
var properties=Object.keys(object);
//if you want to mimic for-in behavior
var i,len;
for(i=0,len=perperties.length;i<len;i++){
console.log("Name:"+properties[i]);
console.log("Value:"+object[properties[i]]);
}
本例使用了Object.keys()獲取了某一個對象的可枚舉屬性。然後用for循環遍歷屬性並輸出它們的名字和值。通常操作一個屬性名數組時會選用Object.keys().而當你不需要數組時則會選用for-in。
注意:for-in 循環返回的和Object()返回的可枚舉屬性有一個區別。for-in循環同時也會遍歷原型屬性而Object.keys()只會返回自有(實例)屬性。
並不是所有的屬性都是可枚舉的。實際上,對象的大部分原生方法的[[Enumerable]]特徵都被質爲false。你可以用propertyIsEnumerable()方法檢查一個屬性是不是可枚舉的。每個對象都有該方法。
var person1={
name:"Nicholas"
};
console.log(:"name" in person1);
console.log(person1.propertyIsEnumerable(“name”);
var perperties =Object.keys(person1);
console.log("length" in perperties);
console.log(properties.propertyIsEnumerable(“length"));
這裏,屬性name是可枚舉的,因爲他是person1的自定義屬性,而properties數組的屬性length則是不可枚舉的。因爲他是array。prototye的內建屬性。你會發現很多原生屬性默認都是不可枚舉的。
3.5 屬性類型
屬性有兩種類型:數據屬性和訪問器屬性。數據屬性包含一個值,例如本章前面幾例的name屬性。[[Put]]方法的默認行爲是創建數據屬性,本章到目前爲止用到的所有例子使用的都是數據屬性。訪問器屬性不包含值而是當定義一個當屬性被讀取時調用的函數(被稱爲getter)和一個當屬性被寫入時調用的函數(稱爲setter)。訪問器屬性僅需要getter或setter兩者中的任意一個,當然也可以兩者都有。
在對象字面形式中定義訪問器屬性有特殊的語法。
var person1={
_name:"Nicholas",
get name(){
console.log("Reading name");
return this._name;},
set name(value){
console.log(“setting name to %s”,value);
};
console.log(person1.name); //Reading name then Nicholas
person1.name="Greg";
console.log(person1.name);
}
本例定義了一個訪問器屬性name。一個數據屬性_name保存了訪問器屬性的實際值。(前置下劃線時一個約定俗稱的命名規範,代表該屬性被認爲是私有的,實際上它還是公開的)用於定義name的getter和setter的語法看上去很想函數但沒有function關鍵字。特殊關鍵字get和set被用在訪問器屬性名字的前面,後面跟着小括號和函數體。getter被期望返回一個值。而setter則接受一個需要被賦給屬性的值作爲參數。
雖然本例使用_name來保存屬性的值,你也可以同樣方便的將數據保存在變量甚至是一個對象中。本例僅僅給屬性的行爲添加了日誌記錄。如果你只是需要保存數據,通常沒有什麼理由使用訪問器屬性--直接使用屬性本身即可。但當你希望複製操作會觸發一些香味或讀取值需要通過計算所需返回值得到時,訪問器屬性會非常有用。
注意:你並不一定要同時定義getter和setter,可以選擇定義其中之一。如果你僅僅定義getter,該屬性就會變成只讀,在非嚴格模式下試圖寫入將失敗,而在嚴格模式下將拋出錯誤。如果你僅僅定義了setter,該屬性就變成只寫,在兩種模式下試圖讀取都將失敗。
3.6 屬性特徵
在ECMAScript5 之前沒有辦法指定一個屬性是否可以枚舉。實際上根本沒有辦法訪問屬性的任何內部特徵。爲了改變這點,ECMAScript 5 引入了多種方法來和屬性特徵直接互動,同時也引入心的特徵來支持額外的功能。現在可以創建出和javascript內建屬性一樣的自定義屬性。本節將詳細介紹數據和訪問器屬性的特徵,下面就從它們的通用特徵開始。
3.6.1 通用特徵
有兩個屬性特徵時數據和訪問器屬性都具有的。一個時[[Enumerable]],決定了你是否可以遍歷該屬性。另一個是[[Configurable]],決定了該屬性是否可以配置。你可以用delete刪除一個可配置的屬性,或隨時改變它。(這也意味着可配置屬性的類型可以從屬據屬性變成訪問器屬性或相反)你聲明的所有屬性默認都是可枚舉,可配置的。
如果你想改變屬性特徵,可以使用Object.defineProperty()方法。該方法接受3個參數:擁有該屬性的對象,屬性名和包含需要設置的特徵的屬性描述對象。屬性描述對象具有和內部特徵同名的屬性但名字不包含中括號。所以你可以用enumaerable屬性來設置[[Enumerable]]特徵,用configurable屬性來設置[[Configrable]]特徵。例如,假設你想要讓一個對象屬性變成不可枚舉且不可配置。
方法如下。
var person1={
name:"Nicholas"
};
Object.defineProperty(person1,"name",{
enumerable:fasle
});
console.log("name" in person1);
console.log(person1.propertyIsEnumerable("name"));
var properties =Object.keys(person1);
console.log(properties.length);
Object.defineProperty(person1,"name",{
configurable:false
});
//try to delete the Property
delete person1.name;
console.log("name" in person1);
console.log(person1.name);
Object.defineProperty(person1,"name",{
configurable:true
});
本例如通常一樣定義來name屬性,然後設置它的[[Enumerable]]特徵爲false,基於這個新值的propertyIsEnumerable()方法返回false。之後name被改爲不可配置。從現在起,由於該屬性不能被改變,試圖刪除name將會失敗,所以name依然存在與person1中。對name再次調用Object.defineProperty()也不會改變屬性。person1對象的屬性name被有效地鎖定。
最後幾行代碼試圖重新定義name爲可配置的。然而這些將拋出錯誤。你無法將一個不可配置的屬性變成可配置的。通用,在不可配置的情況下試圖將數據變爲訪問器屬性或反響變更也會拋出錯誤。
3.6.2 數據屬性特徵
數據屬性額外擁有兩個訪問器屬性不具備的特徵。第一個是[[Value]],包含屬性的值。當你在對象上創建屬性時該特徵被自動賦值。所有屬性的值都保存在[[Value]]中,哪怕該值時一個函數。
第二個特徵時[[Writable]],該特徵時一個布爾值,指示該屬性是否可以寫入。所有的屬性默認都是可寫的,除非你另外指定。
通過這兩個額外特徵,你可以使用Object.defineProperty()完整定義一個數據屬性,即使該屬性還不存在。考慮下例代碼。
var person1={
name:"Nicholas"
};
你已經在本章中多次看到這段代碼,它給person1添加name屬性並設置它的值。你可以用下面(更加羅嗦)的代碼達到同樣的效果
var person1={};
Object.defineProperty(person1,"name",{
value:"Nicholas",
enumerable:true,
configurable:true,
writable:true
});
當Object.defineProperty()被調用時,它首先檢查屬性是否存在。如果不存在,將根據屬性描述對象指定的特徵創建。在本例中。name不是person1已有的屬性。因此它被成功創建。
當你用Object.defineProperty()定義新的屬性時一定記得爲所有的特徵指定一個值,否則布爾型的特徵會被默認設置爲false。下例代碼創建name屬性就是不可枚舉,不可配置,不可寫的,,這是因爲在調用Object.defineProperty()時沒有顯示指定這些特徵值爲true。
var person1={};
Object.defineProperty(person1,"name",{
value:"Nicholas"
});
console.log("name" in person1);
console.log(person1.propertyIsEnumerable("name"));
delete person1.name;
console.log("name" in person1);
person1.name="Greg";
console.log(person1.name);
在這段代碼中,除了讀取name的值,復發對它做任何事情,因爲其他操作都被鎖定了。當你用Object.defineProperty()改變一個已有的屬性時,只有你指定的特徵會被改變。
注意:在嚴格模式下試圖改變不可寫屬性會拋出錯誤,而在非嚴格模式下則會失敗。
3.6.3 訪問器屬性特徵
訪問器屬性也有兩個額外特徵。訪問器屬性不需要儲存值,因此也就沒有[[Value]]和[[Writable]]。取而代之的時[[Get]]和[[Set]],內含getter和setter函數。和對象字面形式的getter和setter一樣,僅需要定義其中一個特徵就可以創建一個訪問器屬性。
注意:如果你試圖創建一個同時具有數據特徵和訪問器特徵的屬性,將會得到一個錯誤。
使用訪問器屬性特徵比使用對象字面形式定義訪問器屬性的優勢在於,你可以爲已有的對象定義這些屬性。如果你想要用對象字面形式,你只能在創建對象時定義訪問器屬性。
和數據屬性一樣,可以指定訪問器屬性是否爲可配置,可枚舉,看看以前的例子,如下。
var person1={
_name:"Nicholas",
get name(){
console.log("Reading name");
return this._name;
},
set name(value){
console.log("Setting name to %s", value);
this._name=value;
};
這段代碼可以被改成如下形式。
var person1={
_name:"Nicholas"
};
Object.defineProperty(person1,"name",{
get:function(){
console.log("Reading name");
return this._name;
},
set:function(value){
console.log("Setting name %s",value);
this._name=value;
},
enumerable:true,
configurable:true
});
}
注意那個傳給Object.defineProperty()的屬性描述對象中的get和set關鍵字,它們時包含函數的數據屬性,在這裏不能使用對象字面形式。
設置[[Enumerable]]和[[Configurable]]可以改變訪問器屬性的工作方式,例如,你可以像下面的代碼一樣來創建一個不可配置,不可枚舉,不可寫的屬性。
var person1={
_name:"Nicholas"
};
Object.defineProperty(person1,"name",{
get:function(){
console.log("Reading name");
return this._name;
});
console.log("name" in person1);
console.log(person1.propertyIsEnumberable("name"));
delete person.name;
console.log("name" in person1);
person1.name="Greg";
console.log(person1.name);l
在這段代碼中,name屬性時一個只有getter的訪問器屬性。沒有setter,也沒有任何特徵被顯式指定爲true,所以它的只只能被讀取,不能被改變。
注意: 和對象字面形式定義的訪問器屬性一樣,在嚴格模式下試圖寫入沒有setter的訪問器屬性會拋出錯誤,而在非嚴格模式下會失敗,試圖讀取一個沒有getter的訪問器則總是返回undefined。
3.6.4 定義多重屬性
如果你使用Object.defineProperties()而不是Object.defineProperty()。可以爲一個對象定義多個屬性。這個訪問接受兩個參數:需要改變的對象和一個包含所有屬性信息的對象。後者可以被看成一個哈希表,鍵名是屬性名,值爲該屬性定義特徵的屬性描述對象。如下代碼定義了兩個屬性。
var person1 ={};
Obejct.defineProperties(person1,{
//data property to store data
_name:{
value:"Nacholas",
enumberable:true,
configurable:true,
writable:true
},
//accessor property
name:{
get:function(){
console.log("Reading name");
return this._name;
},
set:function(value){
console.log("Setting name to %s",value);
this._name=value;},
enumberable:true,
configurable:true
}
});
3.6.5 獲取屬性特徵
如果需要獲取屬性特徵,在JavaScript中可以用Object.getOwnPropertyDescriptor()方法。正如其名,這個方法只可用於自有屬性。它接受兩個參數:對象和屬性名。如果屬性存在,它會返回一個屬性描述對象,內含4個屬性:configurable和enumerable,另外兩個屬性則根據屬性類型決定。即使你從沒有爲屬性顯示指定特徵,你依然會得到包含這些特徵值的屬性描述對象。下面的代碼創建了一個屬性並檢查其他特徵。
var person1={
name:"Nicholas"
};
var descriptor=Object.getOwnPropertyDescriptor(person1,"name");
console.log(descriptor.enumerable);
console.log(descriptor.configurable);
console.log(descriptor.writable);
console.log(descriptor.value);
這裏,屬性name作爲對象字面型號死的一部分被定義,調用Object。getOwnPropertyDescriptor()方法返回的屬性描述對象具有4個屬性:enumerable,configurable,writable和value,即使它們從沒有被Object.defineProperty()顯示定義。
3.7 禁止修改對象
對象和屬性一樣具有指導其行爲的內部特徵,其中,[[Extensible]]是一個布爾值,它指明該對象本身是否可以被修改。你創建的所有對象默認都是可擴展的,意味着新的屬性可以隨時被添加,相信你已經在本章很多地方看到了這一點,設置[[Extensible]]爲false,你就能禁止新屬性的添加。有下列3中方式幫助你鎖定對象屬性。
3.7.1 禁止擴展
第一種方法使用Object.perventExtensions()創建一個不可擴展的對象,該方法接受一個參數,就是你希望使其不可擴展的對象。一旦在一個對象上使用該方法,就永遠不能再給它添加新的屬性了。可以用Object.isExtensible()來檢查[[Extensible]]的值,請看下面的代碼:
var person1={
name:"Nicholas"
};
console.log(Object.isExtensible(person1));
Object.perventExtensions(person1);
person1.sayName=function(){
console.log(this.name);
};
console.log("sayName" in person);
創建person1後,這段代碼先檢查[[Extensible]]特徵,然後將其變得不可擴展,由於不可擴展,sayName()方法永遠無法被加到perosn1()上。
注意:在嚴格模式下試圖給一個不可擴展的對象添加屬性會拋出錯誤,而在非嚴格模式下則會失敗,應該對不可擴展對象使用嚴格模式,這樣,當一個不可擴展對象被錯誤使用時你就會知道。
3.7.2 對象封印
對象封印時創建不可擴展對象的第二種方法。一個被 封印的對象是不可擴展的且其所有屬性都不可配置。這意味着不僅不能給對象添加新屬性,也不能刪除屬性或改變其類型(從數據屬性變成訪問器屬性或相反)。如果一個對象被封印,只能讀寫它的屬性。
可以用Object.seal()方法來封印一個對象。該訪問被調用時,[[Extensible]]特徵被置爲false。其所有屬性的[[Configurable]]特徵被置爲false。如下面圖所示雞,可以用Object.isSealed()判斷一個對象是否別封印。
var person1={
name:“Nicholas”
};
console.log(Object.isExtensible(person1));
console.log(Object.isSealed(person1));
Object.seal(person1);
console.log(Object.isExtensible(person1));
console.log(Object.isSealed(person1));
person1.sayName=function(){
console.log(this.name);
};
console.log("sayName" in person1);
person1.name="Greg";
console.log(person1.name);
delete person1.name;
console.log("name" in person1);
console.log(person1.name);l
var descriptor=Object.getOwnPropertyDescriptor(person1,"name");
console.log(descriptor.configurable);
這段代碼封印了person1,因此不能在person1上添加或刪除屬性。所有別封印對象都是不可擴展的對象,此時對person1使用Objet.isExtensible()方法將會返回false。且試圖添加sayName()會失敗。而且,雖然person1.name被成功改變爲一個新值,但是刪除它將會失敗。
如果你熟悉java或c++,你也應該熟悉被風衣對象,當你基於這兩種語言的類型創建對象時,無法給對象添加新的屬性,單可以修改屬性的值,實際上,封印對象就是javascript在沒有類的情況下允許你做同樣的控制。
注意:確保被封印對象使用嚴格模式。這樣當有人誤用該對象時,你會得到一個錯誤。
3.7.3 對象凍結
創建不可擴展對象的最後一種方法就是凍結它,如果一個對象被凍結,則不能在其上添加或刪除屬性,不能改變屬性類型,也不能寫如任何數據屬性,簡而言之,被凍結對象時一個數據屬性爲只讀的被封印對象。被凍結對象無法解凍。可以用Object。freeze()來凍結一個對象,用Object.isFrozen()來判斷一個對象是否被凍結。代碼如下。
var person1={
name:"Nicholas"
};
console.log(Object.isExtensible(person1));
console.log(Object.isSealed(person1));
console.log(Object.isFrozen(person1));
Object.freeze(person1);
console.log(Object.isExtensible(person1));
console.log(Object.isSealed(person1));
console.log(Object.isFroze(person1));
person1.sayName=function(){
console.log(this.name)};
console.log("sayName" in person1);
person1.name="Greg";
console.log(person1.name);
delete person1.name;
console.log("name" in person1);
console.log(person1.name);
var descriptor=Object.getOwnPropertyDescriptor(person1,"name");
console.log(descriptor.configurable);
console.log(descriptor.writable);
在本例中,person1被凍結,被凍結對象也被認爲時不可擴展對象和被封印對象,所以Object.isExtensible()返回false,而Object.isSealed()怎返回true。屬性name無法被改變,所以試圖對其賦值爲"Greg"的操作失敗,後續的檢查依舊返回"Nicholas".
注意:被凍結對象僅僅只是對象在某個時間點上的快照,其用途有限且極少被使用。和所有不可擴展對象一樣,應該對被凍結對象使用嚴格模式。
3.8 總結
將屬性視爲鍵值對,對象視爲屬性的哈希表有助於理解javascript對象,你可以使用點好或中括號訪問對象的屬性,而可以隨時用賦值的方式添加新屬性,也可以在任何時候用delete操作符刪除一個屬性。你可以隨時用in操作符檢查對象中某個屬性是否存在。如果時自有屬性,還可以使用hasOwnProperty(),這個方法存在於所有對象中。所有對象屬性默認都是可枚舉的,這意外這它們 會出現for-in循環中或者被Object.keys()獲取。
屬性有兩種類型:數據屬性和訪問器屬性。數據屬性保存值,你可以讀寫它們,當數據屬性保存了一個函數的值,該屬性被認爲時對象的一個方法。不同於數據屬性,訪問器屬性不保存值;它們用getter和setter來進行指定的操作,可以用對象字面形式創建數據屬性和訪問器屬性。
所有屬性都有一些相關特徵。這些特徵定義來屬性的工作模式。數據屬性和訪問器屬性都具有[[Enumerable]]和[[Configurable]特徵。數據屬性還具有[[Writable]]和[[Value]]特徵,而訪問器屬性則具有[[Get]]和[[Set]]特徵。[[Enumberable]]和[[Configurable]]默認對所有屬性爲true,[[Writable]]和[[Value]]默認對數據屬性置爲true。你可以用Object.defineProperty() or Object.defineProperties()改變這些特徵,用Object.getOwnPropertyDescriptor()獲取它們。
有三種方式可以鎖定對象的屬性。Object.pervent Extensions()方法創建不可擴展的對象,無法在其上添加新的屬性。如果你用Object.seal()方法創建被封印對象,它不可擴展親愛而屬性不可配置。Obejct.freeze()方法創建被凍結對象,它同時時一個被封印對象且其屬性不可寫,你要當心這些不可擴展對象並始終對它們使用嚴格模式,這樣任何對其錯誤的使用都會拋出一個錯誤。