看《深入理解JavaScript》的this篇時看到“訪問器屬性”這個不熟悉的名詞,百度後找到兩篇感覺比較合適的文章,整合記錄一下,以參考資料2爲主,參考資料1爲輔助補充。
數據屬性
數據屬性包含一個數據值的位置。在這個位置可以讀取和寫入值。數據屬性有 4 個描述其行爲的特性。
- [[Configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲訪問器屬性。
- [[Enumerable]]:表示能否通過 for-in 循環返回屬性。
- [[Writable]]:表示能否修改屬性的值。
- [[Value]]:包含這個屬性的值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值爲 undefined。
數據屬性可以直接定義。對於直接在對象上定義的屬性,它們的 [[Configurable]]、[[Enumerable]] 以及 [[Writable]] 特性都被設置爲 true,而 [[Value]] 特性被設置爲指定的值。
例如定義如下一個對象:
var person = { name: 'hanzichi' };
這裏創建了一個名爲 name 的屬性,爲它指定的值是 'hanzichi'。也就是說,[[Value]] 特性將被設置爲 'hanzichi',而對這個值的任何修改都將反映在這個位置。
要修改默認屬性的特性,必須使用 ES5 的 Object.defineProperty() 方法。這個方法接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符對象的屬性必須是:configurable、enumerable、writable 和 value。例如:
var person = { name: 'hanzichi' }; Object.defineProperty(person, 'name', { value: 'zichi', }); person.name; // => zichi
利用 Object.defineProperty 也可以創建對象的新的屬性。
var person = {}; Object.defineProperty(person, 'name', { value: 'hanzichi' });
如上,person 對象的 name 屬性,其 [[Configurable]]、[[Enumerable]] 以及 [[Writable]] 特性默認爲 false。
例2:
var person = { age:100 }; Object.defineProperty(person,"name",{ configurable:false, writable:false, value:"xiaochang" }); Object.defineProperty(person,"tall",{ value:160 }); for(attr in person){ console.log(attr); //name,age } console.log(person.name); //xiaochang person.name="CC"; //爲name屬性指定新值 console.log(person.name); //xiaochang delete person.name; //刪除name屬性 console.log(person.name); //xiaochang console.log(person.age); //100 person.age=200; //爲age屬性指定新值 console.log(person.age); //200 delete person.age; //刪除age屬性 console.log(person.age); //undefined console.log(person.tall); //160 person.tall = 170; //修改tall屬性的值 console.log(person.tall); //160 delete person.tall; //刪除name屬性 console.log(person.tall); //160
- 分析例子可知直接在對象上定義的屬性,如age,[[Configurable]],[[Enumerable]],[[writable]]都被設置爲true。
- 屬性name的[[Configurable]],[[writable]]被設置爲false,所以無法修改和刪除。
- 調用Object.defineProperty( )方法時,如果不顯示指定configurable,enumerable,writable的值,就默認爲false,如屬性tall。
- 另外需要注意的是當configurable設置爲false後無法再將其改爲true,且除了writable之外,無法修改其它特性。在configurable爲true的情況下可多次調用Object.defineProperty( )修改同一屬性。
- 在非嚴格情況下修改無法配置的屬性操作會被忽略,在嚴格模式下會拋出錯誤。
訪問器屬性
訪問器屬性不包含數據值(沒有 [[Value]] 特性),它們包含一對 getter 和 setter 函數(這兩個函數都不是必須的)。在讀取訪問器屬性時,會調用 getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用 setter 並傳入新值,這個函數負責決定如何處理數據。訪問器屬性有如下 4 個特性。
- [[Configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲數據屬性。
- [[Enumerable]]:表示能否通過 for-in 循環返回屬性。
- [[Get]]:在讀取屬性時調用的函數。默認值爲 undefined。
- [[Set]]:在寫入屬性時調用的函數。默認值爲 undefined。
訪器屬性不能直接定義,必須使用 Object.defineProperty() 來定義。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, 'year', { get: function() { return this._year; }, set: function(newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); book.year = 2005; alert(book.edition); // 2 alert(book.year); // 2005
以上代碼創建了一個 book 對象,並給它定義了兩個默認的屬性:_year 和 edition。_year 前面的下劃線是一種常用的記號,用於表示只能通過對象方法訪問的屬性(雖然理論上是可以直接訪問的)。而訪問器屬性 year 則包含一個 getter 函數和一個 setter 函數。getter 函數返回 _year 的值,setter 函數通過計算來確定正確的版本。因此,把 year 屬性修改爲 2005 會導致 _year 變成 2005,而 edition 變爲 2。這是使用訪問器屬性的常見方式,即設置一個屬性的值會導致其他屬性發生變化。
不一定非要同時指定 getter 和 setter。只指定 getter 意味着屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定了 getter 函數的屬性會拋出錯誤。類似地,只指定 setter 函數的屬性也不能讀,否則在非嚴格模式下返回 undefined,嚴格模式下報錯。
由此可以聯想到數據對象與 DOM 對象的 "雙向綁定"。
Object.defineProperty(user, 'name', { get: function () { return document.getElementById('foo').value; }, set: function (newValue) { document.getElementById('foo').value = newValue; }, configurable: true });
上面代碼使用存取函數,將 DOM 對象 foo 與數據對象 user 的 name 屬性,實現了綁定。兩者之中只要有一個對象發生變化,就能在另一個對象上實時反映出來。
[[Configurable]]
把 configurable 設置爲 false,表示不能從對象中刪除屬性,如果對這個屬性調用 delete,則在非嚴格模式下什麼都不會發生,嚴格模式下報錯。
var person = {}; Object.defineProperty(person, 'name', { value: 'hanzichi', configurable: false // 其實默認就是 false }); delete person.name; alert(person.name); // hanzichi
需要注意的是,當使用 var 命令聲明變量時,變量的 configurable 爲 false。
var a1 = 1; Object.getOwnPropertyDescriptor(this,'a1') // Object { // value: 1, // writable: true, // enumerable: true, // configurable: false // }
而不使用 var 命令聲明變量時(或者使用屬性賦值的方式聲明變量),變量的可配置性爲 true。
a2 = 1; Object.getOwnPropertyDescriptor(this,'a2') // Object { // value: 1, // writable: true, // enumerable: true, // configurable: true // } // 或者寫成 window.a3 = 1; Object.getOwnPropertyDescriptor(window, 'a3') // Object { // value: 1, // writable: true, // enumerable: true, // configurable: true // }
[[Configurable]] 特性還有個作用,一旦把屬性定義爲不可配置的,就不能再把它變爲可配置了。也就是說,當 configurable 爲 false 的時候,value、writable、enumerable 和 configurable 都不能被修改了。
var o = Object.defineProperty({}, 'p', { value: 1, writable: false, enumerable: false, configurable: false }); Object.defineProperty(o,'p', {value: 2}) // TypeError: Cannot redefine property: p Object.defineProperty(o,'p', {writable: true}) // TypeError: Cannot redefine property: p Object.defineProperty(o,'p', {enumerable: true}) // TypeError: Cannot redefine property: p Object.defineProperties(o,'p',{configurable: true}) // TypeError: Cannot redefine property: p
需要注意的是,writable 只有在從 false 改爲 true 會報錯,從 true 改爲 false 則是允許的。
var o = Object.defineProperty({}, 'p', { writable: true, configurable: false }); Object.defineProperty(o,'p', {writable: false}) // 修改成功
至於 value,只要 writable 和 configurable 有一個爲 true,就允許改動。
var o1 = Object.defineProperty({}, 'p', { value: 1, writable: true, configurable: false }); Object.defineProperty(o1,'p', {value: 2}) // 修改成功 var o2 = Object.defineProperty({}, 'p', { value: 1, writable: false, configurable: true }); Object.defineProperty(o2,'p', {value: 2}) // 修改成功
[[Enumerable]]
enumerable 存放一個布爾值,表示該屬性是否可枚舉,如果設爲 false,會使得某些操作(比如 for-in 循環、Object.keys() 以及 JSON.stringify 會跳過該屬性)
var obj = { a: 10, b: 20 }; Object.defineProperty(obj, 'c', { value: 30, enumerable: false }); for (var key in obj) console.log(key, obj[key]); // a 10 // b 20
[[Writable]]
[[Writable]] 特性只存在於數據屬性中,一旦被設置爲 false,那麼該屬性值就不能被修改(只讀)。如果嘗試爲它指定新值,則在非嚴格模式下,會被忽略,嚴格模式下報錯。
var person = {}; Object.defineProperty(person, 'name', { value: 'hanzichi', writable: false // 其實默認就是 false }); person.name = 'zichi'; alert(person.name); // hanzichi
不過如果是用 Object.defineProperty 對屬性重新賦值,就會直接報錯。
var person = {}; Object.defineProperty(person, 'name', { value: 'hanzichi', writable: false // 其實默認就是 false }); Object.defineProperty(person, 'name', { value: 'zichi', }); // Uncaught TypeError: Cannot redefine property: name
當然前面也提到了,如果 writable 爲 false,但是 configurable 爲 true,還是可以對屬性重新賦值的。
其他
我們可以用 Object.defineProperties() 方法同時定義多個屬性。
var person = {}; Object.defineProperties(person, { name: { value: 'hanzichi', writable: true }, age: { value: 30, enumerable: true } });
我們可以用 Object.getOwnPropertyDescriptor() 方法取得給定屬性的描述符。返回是一個對象,如果是數據屬性,這個返回對象的屬性有 configurable、enumerable、writable 以及 value;如果是訪問器屬性,則這個對象的屬性有 configurable、enumerable、get 以及 set。
var person = {}; Object.defineProperty(person, 'name', { value: 'hanzichi', configurable: false // 其實默認就是 false }); var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor); // Object {value: "hanzichi", writable: false, enumerable: false, configurable: false} var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, 'year', { get: function() { return this._year; }, set: function(newValue) { if (newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); Object.getOwnPropertyDescriptor(book, 'year'); // 可以自行打印看看,4 個 key
一個屬性只能是數據屬性或者訪問器屬性,所以均擁有 [[Configurable]] 和 [[Enumerable]] 兩個特性,但是對於其他兩個屬性,則分別擁有,所以不可能同時有 [[Value]] 和 [[Set]],[[Writable]] 和 [[Set]] 等(如果同時定義則會報錯)。