1 概述
JavaScript
提供了一個内部資料結構,用來描述對象的屬性,控制它的行為,比如該屬性是否可寫、可周遊等等。這個内部資料結構稱為“屬性描述對象”(
attributes object
)。每個屬性都有自己對應的屬性描述對象,儲存該屬性的一些元資訊。
例如:
{
value: 123,
writable: false,
enumerable: true,
configurable: false,
get: undefined,
set: undefined
}
2 屬性的元資訊
屬性描述對象提供6個元屬性。
-
:是該屬性的屬性值,預設為value
。undefined
-
:是一個布爾值,表示屬性值(writable
)是否可改變(即是否可寫),預設為value
。true
-
:是一個布爾值,表示該屬性是否可周遊,預設為enumerable
。如果設為false,會使得某些操作(比如for…in循環、Object.keys())跳過該屬性。true
-
:是一個布爾值,表示可配置性,預設為configurable
。如果設為true
,将阻止某些操作改寫該屬性,比如無法删除該屬性,也不得改變該屬性的屬性描述對象(false
屬性除外)。也就是說, configurable 屬性控制了屬性描述對象的可寫性。value
-
:是一個函數,表示該屬性的取值函數(get
),預設為getter
。undefined
-
:是一個函數,表示該屬性的存值函數(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: 新的值必須大于目前值