深入理解javascript物件系列第三篇——神祕的屬性描述符
前面的話
對於作業系統中的檔案,我們可以駕輕就熟將其設定為只讀、隱藏、系統檔案或普通檔案。於物件來說,屬性描述符提供類似的功能,用來描述物件的值、是否可配置、是否可修改以及是否可列舉。本文就來介紹物件中神祕的屬性描述符
描述符型別
物件屬性描述符的型別分為兩種:資料屬性和訪問器屬性
資料屬性
資料屬性(data property)包含一個數據值的位置,在這個位置可以讀取和寫入值。資料屬性有4個特性
可配置性決定是否可以使用delete刪除屬性,以及是否可以修改屬性描述符的特性,預設值為true
可列舉性決定屬性是否出現在物件的屬性列舉中,比如是否可以通過for-in迴圈返回該屬性,預設值為true
可寫性決定是否可以修改屬性的值,預設值為true
【4】Value(屬性值)
屬性值包含這個屬性的資料值,讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值儲存在這個位置。預設值為undefined
訪問器屬性
物件屬性是名字、值和一組屬性描述符構成的。而屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter。而這種屬性型別叫訪問器屬性(accessor property)
可配置性決定是否可以使用delete刪除屬性,以及是否可以修改屬性描述符的特性,預設值為true
可列舉性決定屬性是否出現在物件的屬性列舉中,比如是否可以通過for-in迴圈返回該屬性,預設值為true
在讀取屬性時呼叫的函式。預設值為undefined
在寫入屬性時呼叫的函式。預設值為undefined
和資料屬性不同,訪問器屬性不具有可寫性(Writable)。如果屬性同時具有getter和setter方法,那麼它是一個讀/寫屬性。如果它只有getter方法,那麼它是一個只讀屬性。如果它只有setter方法,那麼它是一個只寫屬性。讀取只寫屬性總是返回undefined
描述符方法
前面介紹了屬性描述符,要想設定它們,就需要用到描述符方法。描述符方法總共有以下4個:
【1】Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor(o,name)方法用於查詢一個屬性的描述符,並以物件的形式返回
查詢obj.a屬性時,可配置性、可列舉性、可寫性都是預設的true,而value是a的屬性值1
查詢obj.b屬性時,因為obj.b屬性不存在,該方法返回undefined
var obj = {a:1}; //Object {value: 1, writable: true, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(obj,'a')); //undefined console.log(Object.getOwnPropertyDescriptor(obj,'b'));
【2】Object.defineProperty()
Object.defineProperty(o,name,desc)方法用於建立或配置物件的一個屬性的描述符,返回配置後的物件
使用該方法建立或配置物件屬性的描述符時,如果不針對該屬性進行描述符的配置,則該項描述符預設為false
var obj = {}; //{a:1} console.log(Object.defineProperty(obj,'a',{ value:1, writable: true })); //由於沒有配置enumerable和configurable,所以它們的值為false //{value: 1, writable: true, enumerable: false, configurable: false} console.log(Object.getOwnPropertyDescriptor(obj,'a'));
【3】Object.defineProperties()
Object.defineProperty(o,descriptors)方法用於建立或配置物件的多個屬性的描述符,返回配置後的物件
var obj = { a:1 }; //{a: 1, b: 2} console.log(Object.defineProperties(obj,{ a:{writable:false}, b:{value:2} })); //{value: 1, writable: false, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(obj,'a')); //{value: 2, writable: false, enumerable: false, configurable: false} console.log(Object.getOwnPropertyDescriptor(obj,'b'));
【4】Object.create()
Object.create(proto,descriptors)方法使用指定的原型和屬性來建立一個物件
var o = Object.create(Object.prototype,{ a:{writable: false,value:1,enumerable:true} }); //{value: 1, writable: false, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(obj,'a'));
描述符詳述
前面分別介紹了資料屬性和訪問器屬性的描述符,但沒有詳細說明其含義及使用,接下來逐一進行說明
可寫性(writable)
可寫性決定是否可以修改屬性的值,預設值為true
var o = {a:1}; o.a = 2; console.log(o.a);//2
設定writable:false後,賦值語句會靜默失效
var o = {a:1}; Object.defineProperty(o,'a',{ writable:false }); console.log(o.a);//1 //由於設定了writable為false,所以o.a=2這個語句會靜默失效 o.a = 2; console.log(o.a);//1 Object.defineProperty(o,'a',{ writable:true }); //由於writable設定為true,所以o.a可以被修改為2 o.a = 2; console.log(o.a);//2
在嚴格模式下通過賦值語句為writable為false的屬性賦值,會提示型別錯誤TypeError
'use strict'; var o = {a:1}; Object.defineProperty(o,'a',{ writable:false }); //Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>' o.a = 2;
[注意]設定writable:false後,通過Object.defineProperty()方法改變屬性value的值不會受影響,因為這也意味著在重置writable的屬性值為false
var o = {a:1}; Object.defineProperty(o,'a',{ writable:false }); console.log(o.a);//1 Object.defineProperty(o,'a',{ value:2 }); console.log(o.a);//2
可配置性(Configurable)
可配置性決定是否可以使用delete刪除屬性,以及是否可以修改屬性描述符的特性,預設值為true
【1】設定Configurable:false後,無法使用delete刪除屬性
var o = {a:1}; Object.defineProperty(o,'a',{ configurable:false }); delete o.a;//false console.log(o.a);//1
在嚴格模式下刪除為configurable為false的屬性,會提示型別錯誤TypeError
'use strict'; var o = {a:1}; Object.defineProperty(o,'a',{ configurable:false }); //Uncaught TypeError: Cannot delete property 'a' of #<Object> delete o.a;
[注意]使用var命令宣告變數時,變數的configurable為false
var a = 1; //{value: 1, writable: true, enumerable: true, configurable: false} Object.getOwnPropertyDescriptor(this,'a');
【2】一般地,設定Configurable:false後,將無法再使用defineProperty()方法來修改屬性描述符
var o = {a:1}; Object.defineProperty(o,'a',{ configurable:false }); //Uncaught TypeError: Cannot redefine property: a Object.defineProperty(o,'a',{ configurable:true });
有一個例外,設定Configurable:false後,只允許writable的狀態從true變為false
var o = {a:1}; Object.defineProperty(o,'a',{ configurable:false, writable:true }); o.a = 2; console.log(o.a);//2 Object.defineProperty(o,'a',{ writable:false }); //由於writable:false生效,物件a的o屬性無法修改值,所以o.a=3的賦值語句靜默失敗 o.a = 3; console.log(o.a);//2
可列舉性(Enumerable)
可列舉性決定屬性是否出現在物件的屬性列舉中,具體來說,for-in迴圈、Object.keys方法、JSON.stringify方法是否會取到該屬性
使用者定義的普通屬性預設是可列舉的,而原生繼承的屬性預設是不可列舉的
//由於原生繼承的屬性預設不可列舉,所以只取得自定義的屬性a:1 var o = {a:1}; for(var i in o){ console.log(o[i]);//1 }
//由於enumerable被設定為false,在for-in迴圈中a屬性無法被枚舉出來 var o = {a:1}; Object.defineProperty(o,'a',{enumerable:false}); for(var i in o){ console.log(o[i]);//undefined }
propertyIsEnumerable()
propertyIsEnumerable()方法用於判斷物件的屬性是否可列舉
var o = {a:1}; console.log(o.propertyIsEnumerable('a'));//true Object.defineProperty(o,'a',{enumerable:false}); console.log(o.propertyIsEnumerable('a'));//false
get和set
get是一個隱藏函式,在獲取屬性值時呼叫。set也是一個隱藏函式,在設定屬性值時呼叫,它們的預設值都是undefined。Object.definedProperty()中的get和set對應於物件字面量中get和set方法
[注意]getter和setter取代了資料屬性中的value和writable屬性
【1】給只設置get方法,沒有設定set方法的物件賦值會靜默失敗,在嚴格模式下會報錯
var o = { get a(){ return 2; } } console.log(o.a);//2 //由於沒有設定set方法,所以o.a=3的賦值語句會靜默失敗 o.a = 3; console.log(o.a);//2
Object.defineProperty(o,'a',{ get: function(){ return 2; } }) console.log(o.a);//2 //由於沒有設定set方法,所以o.a=3的賦值語句會靜默失敗 o.a = 3; console.log(o.a);//2
在嚴格模式下,給沒有設定set方法的訪問器屬性賦值會報錯
'use strict'; var o = { get a(){ return 2; } } console.log(o.a);//2 //由於沒有設定set方法,所以o.a=3的賦值語句會報錯 //Uncaught TypeError: Cannot set property a of #<Object> which has only a getter o.a = 3;
'use strict'; Object.defineProperty(o,'a',{ get: function(){ return 2; } }) console.log(o.a);//2 //由於沒有設定set方法,所以o.a=3的賦值語句會報錯 //Uncaught TypeError: Cannot set property a of #<Object> which has only a getter o.a = 3;
【2】只設置set方法,而不設定get方法,則物件屬性值為undefined
var o = { set a(val){ return 2; } } o.a = 1; console.log(o.a);//undefined
Object.defineProperty(o,'a',{ set: function(){ return 2; } }) o.a = 1; console.log(o.a);//undefined
【3】一般地,set和get方法是成對出現的
var o ={ get a(){ return this._a; }, set a(val){ this._a = val*2; } } o.a = 1; console.log(o.a);//2
Object.defineProperty(o,'a',{ get: function(){ return this._a; }, set :function(val){ this._a = val*2; } }) o.a = 1; console.log(o.a);//2
物件狀態
屬性描述符只能用來控制物件中一個屬性的狀態。而如果要控制物件的狀態,就要用到下面的6種方法
Object.preventExtensions()(禁止擴充套件)
Object.preventExtensions()方法使一個物件無法再新增新的屬性,並返回當前物件
Object.isExtensible()(測試擴充套件)
Object.isExtensible()方法用來檢測該物件是否可以擴充套件
var o = {a:1}; console.log(Object.isExtensible(o));//true o.b = 2; console.log(o);//{a: 1, b: 2} console.log(Object.preventExtensions(o));//{a: 1, b: 2} //由於物件o禁止擴充套件,所以該賦值語句靜默失敗 o.c = 3; console.log(Object.isExtensible(o));//false console.log(o);//{a: 1, b: 2}
在嚴格模式下,給禁止擴充套件的物件新增屬性會報TypeError錯誤
'use strict'; var o = {a:1}; console.log(Object.preventExtensions(o));//{a:1} //Uncaught TypeError: Can't add property c, object is not extensible o.c = 3;
Object.preventExtensions()方法並不改變物件中屬性的描述符狀態
var o = {a:1}; //{value: 1, writable: true, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(o,'a')); Object.preventExtensions(o); //{value: 1, writable: true, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(o,'a'));
Object.seal()(物件封印)
物件封印又叫物件密封,使一個物件不可擴充套件並且所有屬性不可配置,並返回當前物件
Object.isSealed()(測試封印)
Object.isSealed()方法用來檢測該方法是否被封印
var o = {a:1,b:2}; console.log(Object.isSealed(o));//false console.log(Object.seal(o));//{a:1,b:2} console.log(Object.isSealed(o));//true console.log(delete o.b);//false o.c = 3; console.log(o);//{a:1,b:2}
在嚴格模式下,刪除舊屬性或新增新屬性都會報錯
'use strict'; var o = {a:1,b:2}; console.log(Object.seal(o));//{a:1,b:2} //Uncaught TypeError: Cannot delete property 'b' of #<Object> delete o.b;
這個方法實際上會在現有物件上呼叫Object.preventExtensions()方法,並把所有現有屬性的configurable描述符置為false
var o = {a:1,b:2}; //{value: 1, writable: true, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(o,'a')); console.log(Object.seal(o));//{a:1,b:2} //{value: 1, writable: true, enumerable: true, configurable: false} console.log(Object.getOwnPropertyDescriptor(o,'a'));
Object.freeze()(物件凍結)
Object.freeze()方法使一個物件不可擴充套件,不可配置,也不可改寫,變成一個僅可以列舉的只讀常量,並返回當前物件
Object.isFrozen()(檢測凍結)
Object.isFrozen()方法用來檢測一個物件是否被凍結
var o = {a:1,b:2}; console.log(Object.isFrozen(o));//false console.log(Object.freeze(o));//{a:1,b:2} console.log(Object.isFrozen(o));//true o.a = 3; console.log(o);//{a:1,b:2}
在嚴格模式下,刪除舊屬性、新增新屬性、更改現有屬性都會報錯
'use strict'; var o = {a:1,b:2}; console.log(Object.freeze(o));//{a:1,b:2} //Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>' o.a = 3;
這個方法實際上會在現有物件上呼叫Object.seal()方法,並把所有現有屬性的writable描述符置為false
var o = {a:1}; //{value: 1, writable: true, enumerable: true, configurable: true} console.log(Object.getOwnPropertyDescriptor(o,'a')); console.log(Object.freeze(o));//{a:1} //{value: 1, writable: false, enumerable: true, configurable: false} console.log(Object.getOwnPropertyDescriptor(o,'a'));
參考資料
【1】 阮一峰Javascript標準參考教程——屬性描述物件 http://javascript.ruanyifeng.com/stdlib/attributes.html
【2】《javascript權威指南(第6版)》第6章 物件
【3】《你不知道的javascript上卷》第3章 物件
【4】《javascript高階程式設計(第3版)》第6章 面向物件的程式設計
【5】《javascript面向物件精要》 第3章 理解物件