天天看點

Object屬性的描述對象(Descriptor)

1 概述

​JavaScript ​

​提供了一個内部資料結構,用來描述對象的屬性,控制它的行為,比如該屬性是否可寫、可周遊等等。這個内部資料結構稱為“屬性描述對象”(​

​attributes object​

​​)。每個屬性都有自己對應的屬性描述對象,儲存該屬性的一些元資訊。

例如:

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}      

2 屬性的元資訊

屬性描述對象提供6個元屬性。

  1. ​value​

    ​:是該屬性的屬性值,預設為​

    ​undefined​

    ​。
  2. ​writable​

    ​​:是一個布爾值,表示屬性值(​

    ​value​

    ​)是否可改變(即是否可寫),預設為​

    ​true​

    ​。
  3. ​enumerable​

    ​:是一個布爾值,表示該屬性是否可周遊,預設為​

    ​true​

    ​。如果設為false,會使得某些操作(比如for…in循環、Object.keys())跳過該屬性。
  4. ​configurable​

    ​:是一個布爾值,表示可配置性,預設為​

    ​true​

    ​​。如果設為​

    ​false​

    ​​,将阻止某些操作改寫該屬性,比如無法删除該屬性,也不得改變該屬性的屬性描述對象(​

    ​value​

    ​屬性除外)。也就是說, configurable 屬性控制了屬性描述對象的可寫性。
  5. ​get​

    ​​:是一個函數,表示該屬性的取值函數(​

    ​getter​

    ​​),預設為​

    ​undefined​

    ​。
  6. ​set​

    ​​:是一個函數,表示該屬性的存值函數(​

    ​setter​

    ​​),預設為​

    ​undefined​

    ​。

2.1 value

​value​

​屬性是目标屬性的值。

var obj = {};
obj.p = 123;

Object.getOwnPropertyDescriptor(obj, 'p').value
// 123

Object.defineProperty(obj, 'p', { value: 246 });
obj.p // 246      

2.2 writable

​writable​

​​屬性是一個布爾值,決定了目标屬性的值(​

​value​

​)是否可以被改變。

var obj = {};

Object.defineProperty(obj, 'a', {
  value: 37,
  writable: false
});

obj.a // 37
obj.a = 25;
obj.a // 37
// obj.a的writable屬性是false。然後,改變obj.a的值,不會有任何效果。      

【注意】:正常模式下,對​

​writable​

​​為​

​false​

​的屬性指派不會報錯,隻會默默失敗。但是,嚴格模式下會報錯,即使對​

​a​

​屬性重新賦予一個同樣的值。

'use strict';
var obj = {};

Object.defineProperty(obj, 'a', {
  value: 37,
  writable: false
});

obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property 'a' of object      

如果原型對象的某個屬性的​

​writable​

​​為​

​false​

​,那麼子對象将無法自定義這個屬性。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var obj = Object.create(proto);

obj.foo = 'b';
obj.foo // 'a'      

​proto​

​​是原型對象,它的​

​foo​

​​屬性不可寫。​

​obj​

​​對象繼承​

​proto​

​​,也不可以再自定義這個屬性了。如果是嚴格模式,這樣做還會抛出一個錯誤。

但是,有一個規避方法,就是通過覆寫屬性描述對象,繞過這個限制。原因是這種情況下,原型鍊會被完全忽視。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', {
  value: 'b'
});

obj.foo // "b"      

2.3 enumerable

​enumerable​

​​(可周遊性)傳回一個布爾值,表示目标屬性是否可周遊。

​​

​JavaScript ​

​​的早期版本,​

​for...in​

​​循環是基于​

​in​

​​運算符的。我們知道,​

​in​

​​運算符不管某個屬性是對象自身的還是繼承的,都會傳回​

​true​

​。

var obj = {};
'toString' in obj // true      

上面代碼中,​

​toString​

​​不是​

​obj​

​​對象自身的屬性,但是​

​in​

​​運算符也傳回​

​true​

​​,這導緻了​

​toString​

​​屬性也會被​

​for...in​

​​循環周遊。

這顯然不太合理,後來就引入了“可周遊性”這個概念。隻有可周遊的屬性,才會被​​

​for...in​

​​循環周遊,同時還規定​

​toString​

​​這一類執行個體對象繼承的原生屬性,都是不可周遊的,這樣就保證了​

​for...in​

​循環的可用性。

具體來說,如果一個屬性的​

​enumerable​

​​為​

​false​

​,下面三個操作不會取到該屬性。

  • ​for..in​

    ​循環
  • ​Object.keys​

    ​方法
  • ​JSON.stringify​

    ​​方法

    是以,​​

    ​enumerable​

    ​可以用來設定“秘密”屬性。
var obj = {};

Object.defineProperty(obj, 'x', {
  value: 123,
  enumerable: false
});

obj.x // 123

for (var key in obj) {
  console.log(key);
}
// undefined

Object.keys(obj)  // []
JSON.stringify(obj) // "{}"      

上面代碼中,​

​obj.x​

​​屬性的​

​enumerable​

​​為​

​false​

​​,是以一般的周遊操作都無法擷取該屬性,使得它有點像“秘密”屬性,但不是真正的私有屬性,還是可以直接擷取它的值。

【注意】:​​

​for...in​

​​循環包括繼承的屬性,​

​Object.keys​

​​方法不包括繼承的屬性。如果需要擷取對象自身的所有屬性,不管是否可周遊,可以使用​

​Object.getOwnPropertyNames​

​​方法。

另外,​​

​JSON.stringify​

​​方法會排除​

​enumerable​

​​為​

​false​

​​的屬性,有時可以利用這一點。如果對象的 ​

​JSON ​

​​格式輸出要排除某些屬性,就可以把這些屬性的​

​enumerable​

​​設為​

​false​

​。

2.4 configurable

​configurable​

​​(可配置性)傳回一個布爾值,決定了是否可以修改屬性描述對象。也就是說,​

​configurable​

​​為​

​false​

​​時,​

​value​

​​、​

​writable​

​​、​

​enumerable​

​​和​

​configurable​

​都不能被修改了。

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false
});

Object.defineProperty(obj, 'p', {value: 2})
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, 'p', {writable: true})
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, 'p', {enumerable: true})
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, 'p', {configurable: true})
// TypeError: Cannot redefine property: p      

上面代碼中,​

​obj.p​

​​的​

​configurable​

​​為​

​false​

​​。然後,改動​

​value​

​​、​

​writable​

​​、​

​enumerable​

​​、​

​configurable​

​​,結果都報錯。

【注意】:​​

​writable​

​​隻有在​

​false​

​​改為​

​true​

​​會報錯,​

​true​

​​改為​

​false​

​是允許的。

var obj = Object.defineProperty({}, 'p', {
  writable: true,
  configurable: false
});

Object.defineProperty(obj, '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})
// 修改成功      

另外,​

​writable​

​​為​

​false​

​時,直接目标屬性指派,不報錯,但不會成功。

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  configurable: false
});

obj.p = 2;
obj.p // 1      

同時,可配置性決定了目标屬性是否可以被删除(​

​delete​

​)。

var obj = Object.defineProperties({}, {
  p1: { value: 1, configurable: true },
  p2: { value: 2, configurable: false }
});

delete obj.p1 // true
delete obj.p2 // false

obj.p1 // undefined
obj.p2 // 2      

上面代碼中,​

​obj.p1​

​​的​

​configurable​

​​是​

​true​

​,是以可以被删除,​

​obj.p2​

​就無法删除。

2.5 存取器

除了直接定義以外,屬性還可以用存取器(​

​accessor​

​​)定義。其中,存值函數稱為​

​setter​

​​,使用屬性描述對象的​

​set​

​​屬性;取值函數稱為​

​getter​

​​,使用屬性描述對象的​

​get​

​​屬性。

一旦對目标屬性定義了存取器,那麼存取的時候,都将執行對應的函數。利用這個功能,可以實作許多進階特性,比如某個屬性禁止指派。

var obj = Object.defineProperty({}, 'p', {
  get: function () {
    return 'getter';
  },
  set: function (value) {
    console.log('setter: ' + value);
  }
});

obj.p // "getter"
obj.p = 123 // "setter: 123"      

上面代碼中,​

​obj.p​

​​定義了get和set屬性。​

​obj.p​

​​取值時,就會調用​

​get​

​​;指派時,就會調用​

​set​

​​。

​​

​JavaScript ​

​還提供了存取器的另一種寫法。

var obj = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  }
};      
var obj ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw new Error('新的值必須大于目前值');
  }
};

obj.next // 5

obj.next = 10;
obj.next // 10

obj.next = 5;
// Uncaught Error: 新的值必須大于目前值      

繼續閱讀