如我們所知,物件可以儲存屬性。
到目前為止,對我們來說,屬性只是一個簡單的「鍵值」配對。但物件屬性實際上是一個更靈活且強大的東西。
在本章中,我們將研究其他設定選項,在下一章中,我們將了解如何將它們隱形地轉換為 getter/setter 函式。
屬性標記
物件屬性除了值
之外,還有三個特殊屬性(稱為「標記」)
可寫入
– 如果為true
,則可以變更值,否則為唯讀。可列舉
– 如果為true
,則會列在迴圈中,否則不會列出。configurable
– 如果為true
,則可以刪除屬性,並修改這些屬性,否則不行。
我們還沒有看到它們,因為它們通常不會顯示。當我們「以一般方式」建立屬性時,它們都是 true
。但是我們也可以隨時變更它們。
首先,讓我們看看如何取得這些標記。
方法 Object.getOwnPropertyDescriptor 允許查詢屬性的完整資訊。
語法為
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
- 用於取得資訊的物件。
propertyName
- 屬性的名稱。
傳回的值是一個所謂的「屬性描述」物件:它包含值和所有標記。
例如
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
若要變更標記,我們可以使用 Object.defineProperty。
語法為
Object.defineProperty(obj, propertyName, descriptor)
obj
、propertyName
- 用於套用描述的物件及其屬性。
descriptor
- 要套用的屬性描述物件。
如果屬性存在,defineProperty
會更新其標記。否則,它會使用指定的值和標記建立屬性;在這種情況下,如果未提供標記,則假設為 false
。
例如,這裡建立了一個具有所有假標記的屬性 name
let user = {};
Object.defineProperty(user, "name", {
value: "John"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
"writable": false,
"enumerable": false,
"configurable": false
}
*/
將它與上面「正常建立」的 user.name
進行比較:現在所有標記都是假的。如果這不是我們想要的,那麼我們最好在 descriptor
中將它們設定為 true
。
現在讓我們透過範例看看標記的效果。
不可寫入
讓我們透過變更 writable
標記,使 user.name
不可寫入(無法重新指派)
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "Pete"; // Error: Cannot assign to read only property 'name'
現在,除非其他人套用自己的 defineProperty
來覆寫我們的 defineProperty
,否則沒有人可以變更我們使用者的名稱。
在非嚴格模式下,寫入不可寫入屬性等時不會發生錯誤。但是操作仍然不會成功。在非嚴格模式下,違反標記的動作只會被靜默忽略。
以下是相同的範例,但是屬性從頭建立
let user = { };
Object.defineProperty(user, "name", {
value: "John",
// for new properties we need to explicitly list what's true
enumerable: true,
configurable: true
});
alert(user.name); // John
user.name = "Pete"; // Error
不可列舉
現在讓我們為 `user` 新增一個自訂的 `toString`。
一般來說,物件內建的 `toString` 是不可列舉的,它不會出現在 `for..in` 中。但如果我們新增一個自訂的 `toString`,那麼預設它會出現在 `for..in` 中,如下所示
let user = {
name: "John",
toString() {
return this.name;
}
};
// By default, both our properties are listed:
for (let key in user) alert(key); // name, toString
如果我們不喜歡這樣,那麼可以設定 `enumerable:false`。這樣它就不會出現在 `for..in` 迴圈中,就像內建的一樣
let user = {
name: "John",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
enumerable: false
});
// Now our toString disappears:
for (let key in user) alert(key); // name
不可列舉的屬性也會從 `Object.keys` 中排除
alert(Object.keys(user)); // name
不可設定
不可設定標記 (configurable:false) 有時會預設設定在內建物件和屬性上。
不可設定的屬性無法刪除,它的屬性也無法修改。
例如,`Math.PI` 是不可寫入、不可列舉且不可設定的
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": 3.141592653589793,
"writable": false,
"enumerable": false,
"configurable": false
}
*/
因此,程式設計師無法變更 `Math.PI` 的值或覆寫它。
Math.PI = 3; // Error, because it has writable: false
// delete Math.PI won't work either
我們也無法將 `Math.PI` 再次變更為 `writable`。
// Error, because of configurable: false
Object.defineProperty(Math, "PI", { writable: true });
我們對 `Math.PI` 絕對無能為力。
將屬性設為不可設定是一條單行道。我們無法使用 `defineProperty` 將它變更回來。
請注意:`configurable: false` 會防止屬性標記變更和刪除,但允許變更它的值。
這裡的 `user.name` 是不可設定的,但我們仍然可以變更它 (因為它是可寫入的)
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
configurable: false
});
user.name = "Pete"; // works fine
delete user.name; // Error
而這裡我們將 `user.name` 設為「永遠密封」的常數,就像內建的 `Math.PI` 一樣
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false,
configurable: false
});
// won't be able to change user.name or its flags
// all this won't work:
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Pete" });
關於變更標記有一個小例外。
我們可以將不可設定屬性的 `writable: true` 變更為 `false`,從而防止它的值被修改 (以增加另一層保護)。但不能反過來。
Object.defineProperties
有一個方法 Object.defineProperties(obj, descriptors) 允許一次定義多個屬性。
語法為
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
例如
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
因此,我們可以一次設定多個屬性。
Object.getOwnPropertyDescriptors
要一次取得所有屬性描述,我們可以使用 Object.getOwnPropertyDescriptors(obj) 方法。
它可以與 `Object.defineProperties` 一起用作「標記感知」的物件複製方式
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
一般來說,當我們複製一個物件時,我們會使用賦值來複製屬性,如下所示
for (let key in user) {
clone[key] = user[key]
}
…但這樣不會複製標記。因此,如果我們想要一個「更好的」複製,那麼建議使用 `Object.defineProperties`。
另一個不同點是,for..in
會忽略符號和不可列舉的屬性,但 Object.getOwnPropertyDescriptors
會傳回所有的屬性描述,包括符號和不可列舉的屬性。
封裝一個物件
屬性描述會在個別屬性的層級運作。
還有方法可以限制對整個物件的存取
- Object.preventExtensions(obj)
- 禁止新增新的屬性到物件中。
- Object.seal(obj)
- 禁止新增/移除屬性。將所有現有屬性的
configurable: false
設為true
。 - Object.freeze(obj)
- 禁止新增/移除/變更屬性。將所有現有屬性的
configurable: false, writable: false
設為true
。
還有對它們的測試
- Object.isExtensible(obj)
- 如果禁止新增屬性,傳回
false
,否則傳回true
。 - Object.isSealed(obj)
- 如果禁止新增/移除屬性,且所有現有屬性都有
configurable: false
,傳回true
。 - Object.isFrozen(obj)
- 如果禁止新增/移除/變更屬性,且所有現有屬性都有
configurable: false, writable: false
,傳回true
。
這些方法在實務上很少使用。
留言
<code>
標籤,要插入多行,請用<pre>
標籤包起來,要插入超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)