JavaScript數據屬性和訪問器屬性

看《深入理解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]] 等(如果同時定義則會報錯)。

參考資料

JavaScript筆記--數據屬性和訪問器屬性

JavaScript 屬性類型(數據屬性和訪問器屬性)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章