在ES5規范中,還有一個比較重要的改進,就是Object對象的增強,ES5為Object新增了一系列函數,用於編寫安全健壯的程序,今天我們就來一一介紹它們的使用方法。
下面就是ES5中Object新增的函數:
Object.defineProperty(object, propertyName, descriptor);
Object.defineProperties(object, descriptors);
Object.getOwnPropertyDescriptor(object, propertyName);
Object.create(prototype, descriptors);
Object.getPrototypeOf(object);
Object.keys(object);
Object.getOwnPropertyNames(object);
Object.preventExtensions(object);
Object.isExtensible(object);
Object.seal(object);
Object.isSealed(object);
Object.freeze(object);
Object.isFrozen(object);
下面我們逐一介紹:
Object.defineProperty(object, propertyName, descriptor);
defineProperty函數用於定義一個對象上的屬性以及這個屬性的描述符。這裡涉及到描述符這個概念,需要先理解一下,描述符是用來描述一個屬性的值和這個屬性在運行期的訪問控制,一個描述符包含下面幾個聲明:
configurable: 表示是否能通過delete刪除對象中的該屬性,以及是否能重新定義該屬性,默認是false,默認情況下,不能用delete刪除屬性,不能重新定義該屬性。需要注意的是,一旦明確設置configurable為false之後,就再也不能重新設置這個規則為true了。
enumerable: 表示能否通過for-in循環獲得對象中的該屬性,默認值是false,默認情況下,使用for-in循環將不能看到該屬性出現。
writable: 表示能否修改該屬性,默認值是false,默認情況下,不能再更改該屬性的值。
value: 該屬性的值,默認值是undefined。通常我們會設置value為一個有意義的值。
下面是每個聲明的默認值列表:(Get和Set後續會介紹)
這裡需要注意,當configurable和writable為false的情況下,試圖刪除或更改相對應的屬性,常規模式下操作將會被忽略,如果是嚴格模式,將會拋出異常。我們的代碼會加上"use strict";來啟用嚴格模式。關於嚴格模式的細節,我們後續的文章會專門介紹。
我們先來使用defineProperty函數定義一個對象的屬性:
"use strict"; var person = {}; Object.defineProperty(person, 'name', { value: 'Scott' }); person.name = 'John';上面的代碼我們使用defineProperty為person對象定義了一個name屬性,然後試圖更改它的值。如果在常規模式下運行,更改操作將會被忽略,這裡我們加上了嚴格模式的聲明,將會拋出下面的異常:
如果通過delete試圖刪除name屬性,同樣也會得到一個異常結果:
delete person.name;
<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+ztLDx8nUzqK4xLav0rvPwrT6wuujrM6qZGVmaW5lUHJvcGVydHm6r8r9tcTX7rrz0ru49rLOyv3M7bzT0ru49ndyaXRhYmxlyfnD96O6PC9wPgo8cD48L3A+CjxwcmUgY2xhc3M9"brush:java;">"use strict"; var person = {}; Object.defineProperty(person, 'name', { writable: true, //add writable as true value: 'Scott' }); person.name = 'John'; console.log('after changing the name: ', person.name); delete person.name; console.log('after deleting the name: ', person.name);添加writable為true後,再次運行程序,看看結果如何:
我們現在可以更改name屬性的值了。只不過試圖刪除name時,仍然會拋出一個異常,現在我們需要再添加一個configurable聲明:
"use strict"; var person = {}; Object.defineProperty(person, 'name', { configurable: true, //add configurable as true writable: true, //add writable as true value: 'Scott' }); person.name = 'John'; console.log('after changing the name: ', person.name); delete person.name; console.log('after deleting the name: ', person.name);
從打印結果來看,更改操作和刪除操作在嚴格模式下都順利執行了。現在大家應該都了解到configurable和writable的作用了吧。configurable允許或禁止刪除操作的執行,writable允許或禁止更改操作的執行。
另外,上面我們也提到過,如果明確聲明了configurable為false,則不能再使用defineProperty將其定義configurable為true了,下面代碼將會拋出一個異常:
'use strict'; var person = {}; Object.defineProperty(person, 'name', { configurable: false, //declare configurable as false writable: false, //declare writable as false value: 'Scott' }); //try to redefine the name's descriptor Object.defineProperty(person, 'name', { configurable: true, writable: true, value: 'Scott' });
當外部代碼試圖更改對象屬性時,適當的使用configurable和writable為其加一些限制,可以提高代碼的安全性,這一點對模塊開發非常有用。
下面來介紹一下enumerable聲明,我們用一個簡單的示例來講解:
"use strict"; var person = {}; Object.defineProperty(person, 'name', { value: 'Scott' }); //nothing will be logged for (var key in person) { if (person.hasOwnProperty(key)) { console.log(key + ': ' + person[key]); } }上面這段代碼我們不會看到person中的鍵值對,因為默認情況下使用defineProperty定義屬性時,屬性的enumerable為false,即不可被遍歷,如果需要在for-in中獲取到name屬性,我們需要為其聲明enumerable為true:
"use strict"; var person = {}; Object.defineProperty(person, 'name', { enumerable: true, //add enumerable as true value: 'Scott' }); //name: Scott for (var key in person) { if (person.hasOwnProperty(key)) { console.log(key + ': ' + person[key]); } }
檢測一個對象的屬性是否可遍歷,我們還可以使用Object的另外一個原型方法,它更為方便:
var isNameEnumerable = person.propertyIsEnumerable('name');除了上面這些之外,在defineProperty函數的descriptor中還可以使用setter和getter對屬性進行定義:
var getPerson = function() { var person = {}; var personAge = 20; Object.defineProperty(person, 'age', { get: function() { return personAge; }, set: function(newAge) { if (newAge < 0) { newAge = 0; } if (newAge > 150) { newAge = 150 } personAge = newAge; } }); return person; } var person = getPerson(); person.age = -1; console.log(person.age); //0 person.age = 200; console.log(person.age); //150
上面代碼中我們把創建person對象的操作封裝在getPerson函數中,然後使用set方法和get方法定義age屬性的行為,在set和get方法中,間接使用了局部變量personAge來表示person的age屬性,這種方式的好處在於,可以對賦值操作加一些驗證,使其符合我們業務邏輯的要求。不過需要注意的是,不是一定非要同時指定setter和getter的,在只定義getter時該屬性不能寫,只指定setter時不能讀,如果只有getter而嘗試去寫、只有setter嘗試去讀的話,嚴格模式下會拋出異常,所以正確地使用setter和getter才能寫出高質量的代碼。
在了解上面介紹的defineProperty函數之後,對於這個函數就比較容易理解了,defineProperties函數用於一次性定義多個屬性,我們用一段代碼來解釋:
var person = {}; Object.defineProperties(person, { 'name': { writable: false, value: 'Scott' }, 'address': { writable: true, value: 'Beijing' } });
代碼中我們最後一個參數包含兩個屬性:name和address,分別都有自己的屬性描述信息,這種方式比較單一屬性的聲明來說簡單明了,在一次性有多個屬性需要聲明時比較實用。
var person = {}; Object.defineProperty(person, 'name', { configurable: true, writable: false, enumerable: true, value: 'Scott' }); var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor.configurable); //true console.log(descriptor.writable); //false console.log(descriptor.enumerable); //true console.log(descriptor.value); //Scott
需要注意的是,如果定義屬性時使用了set和get方法,那麼返回的descriptor對象也是可以通過set和get訪問到相應的setter和getter的。
接著我們來了解一下與原型有關的兩個函數:
Object.create(prototype, descriptors);
create函數用於在指定原型基礎之上創建一個新的對象,第一個參數即指定的原型對象,第二個參數就是我們上面介紹到的描述符,裡面可以定義多個屬性的描述信息。原型參數只能是一個對象或者指定為null,下面三種方式都可以創建一個空對象:
var obj0 = Object.create({}); var obj1 = Object.create(null); var obj2 = Object.create(Object.prototype);
我們當然還可以指定一個包含多個屬性描述信息的描述符參數,來創建一個新的對象,在原來對象上進行擴展。下面這個例子我們在一個已有對象的基礎之上創建一個person對象,包含name屬性和gender屬性,其中name可更改不可刪除,gender是只讀狀態:
var human = { info: 'human being' }; var person = Object.create(human, { name: { configurable: false, writable: true, enumerable: true, value: 'Scott' }, gender: { get: function() { return "Male"; } } }); console.log(person);上面代碼相當於在human對象上進行擴展,添加了name和gender屬性,進而創建一個新對象,我們來看一下person對象的結構:
從打印信息中,我們可以看出,因為name屬性是可遍歷的,所以顯示在Object{}中,當我們展開後,會看到gender的get方法,也可以看得到person的原型鏈,包含info屬性的原型就是我們上面定義的human對象了,證明person對象確實是human對象的擴展。create與defineProperties有些相象之處,不同的是,create會創建一個新的對象,而defineProperties不會,我們也可以獲取它的返回值,返回值就是原對象本身。
Object.getPrototypeOf(object);
此函數用於獲取指定對象的原型,來看下面這個精簡過的例子:
var human = { info: 'human being' }; var person = Object.create(human, { name: { value: 'Scott' } }); console.log(Object.getPrototypeOf(person) === human); //true我們使用human作為原型創建一個person對象,然後使用getPrototypeOf函數獲取person對象的原型,結果會返回human,我們上面也介紹到了,person對象的確是從human對象擴展而來,getPrototypeOf獲取到的原型和我們期望的是一致的。
此函數用於獲取對象中可被遍歷的全部屬性的key的集合。看下面例子:
var array = ['a', 'b', 'c']; console.log(Object.keys(array)); //["0", "1", "2"] var person = Object.defineProperties({}, { name: { enumerable: true, value: 'Scott' }, info: { enumerable: true, value: 'I am Scott' }, address: { enumerable: false, //not enumerable value: 'Beijing' } }); console.log(Object.keys(person)); //["name", "info"]首先我們創建一個數組,使用keys函數獲取所有的key,結果會打印出數組的下標組成的集合;然後我們定義了person對象的屬性,其中name和info都是可遍歷的,address是不可遍歷的,結果會打印出name和info,而不會出現address,使用keys函數的時候需要注意這一點。
Object.getOwnPropertyNames(object);
此函數與keys函數相似,不過它會返回所有的鍵,包括哪些不可被遍歷的屬性。現在我們把keys函數替換成getOwnPropertyNames,看看結果如何:
var array = ['a', 'b', 'c']; console.log(Object.getOwnPropertyNames(array)); var person = Object.defineProperties({}, { name: { enumerable: true, value: 'Scott' }, info: { enumerable: true, value: 'I am Scott' }, address: { enumerable: false, //not enumerable value: 'Beijing' } }); console.log(Object.getOwnPropertyNames(person));結果打印如下:
我們會發現,array裡面增加了一個length的key,而person對象也增加了一個address的key,我們可以得知,數組對象的length被設置為不可遍歷的,也證明了getOwnPropertyNames確實可以把所有的屬性的key獲取到。
Object.preventExtensions(object); & Object.isExtensible(object);
preventExtensions函數用於禁止指定對象的擴展,即禁止向對象中添加新的屬性。我們定義一個簡單的對象,然後測試一下這個函數:
'use strict'; var person = { name: 'Scott' }; Object.preventExtensions(person); console.log(Object.isExtensible(person)); //false //Uncaught TypeError: Can't add property age, object is not extensible person.age = 20;測試證明,試圖添加一個age屬性,嚴格模式下將會拋出異常,證明person對象已經是不可擴展的。
Object.seal(object); &Object.isSealed(object);
seal函數用於對指定對象進行封存,已封存的對象禁止再添加新的屬性,禁止刪除已有的屬性,禁止重新定義已有屬性的cnofigurable為true。來看下面一段代碼:
'use strict'; var person = { name: 'Scott' }; Object.seal(person); console.log(Object.isSealed(person)); //true person.name = 'John'; //Uncaught TypeError: Can't add property age, object is not extensible //person.age = 20; //Uncaught TypeError: Cannot delete property 'name' of #
上面我們定義了一個普通字面量的person對象,它的name屬性是可寫可刪除可遍歷的,接著我們調用seal函數對其進行封存,如果我們想要判斷一個對象是否已被封存,可以使用isSealed函數。被封存的對象禁止添加屬性和刪除已有的屬性,所以我們試圖添加一個age屬性和刪除已有的name屬性都會在嚴格模式下拋出異常。需要注意的是,只要是可寫的屬性,即使被封存也可以更改其值,所以更改name的值是不會有問題的。
另外,對已被封存對象的屬性重新定義也要特別小心,因為被封存的對像使用defineProperty重新定義規則時,只能更改writable和value,不能更改原來的enumerable,對於configurable更為嚴苛,不能明確聲明。如下代碼列舉了一些注意事項:
'use strict'; var person = { name: 'Scott' }; Object.seal(person); person.name = 'Jack'; console.log(person.name); //Jack //after sealed, can't declare the configurable, and can't change the original enumerable. Object.defineProperty(person, 'name', { //configurable: true, writable: false, enumerable: true, //the enumerable here must be equal to the original if declare explicitly value: 'John' }); //Uncaught TypeError: Cannot assign to read only property 'name' of object '#可以看出writable是可以更改為false的,但注意enumerable只能和原來對象一樣為true,而configurable在這裡不能明確聲明,大家可以親自測試一下。
Object.freeze(object); & Object.isFrozen(object);
freeze函數用於對指定對象進行凍結,凍結後的對象禁止添加屬性,禁止刪除屬性,禁止更改屬性,禁止使用defineProperty更改其configurable,writable,和enumerable的值。可以說freeze函數禁止對象屬性上的所有操作,不過需要注意的是,如果對象的屬性也是對象類型,那麼這個屬性對象下面的屬性是不會被凍結的,我們依舊可以操作:
'use strict'; var person = { name: 'Scott', info: { weight: 65, height: 175 } }; Object.freeze(person); console.log(Object.isFrozen(person)); //true person.info.age = 20; //add 'age' property to info person.info.weight = 70; //change the 'weight' property
上面代碼中,我們向info中添加age屬性,或者更改weight的值,這些操作都是沒有問題的,所以如果需要對一個對象及下面的屬性進行完全的凍結,我們需要遞歸的對每一個屬性對象進行凍結。
以上就是ES5對Object的增強,有了這些強大的API,可以確保我們的代碼更加安全,進而可以構建出健壯的應用。另外,在上面的示例中,我們多次提到了嚴格模式,這也是ES5規范中重要的一個方面,下次我們會對嚴格模式進行一個全面的概括。