2022 年 10 月 18 日

屬性標記和描述符

如我們所知,物件可以儲存屬性。

到目前為止,對我們來說,屬性只是一個簡單的「鍵值」配對。但物件屬性實際上是一個更靈活且強大的東西。

在本章中,我們將研究其他設定選項,在下一章中,我們將了解如何將它們隱形地轉換為 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)
objpropertyName
用於套用描述的物件及其屬性。
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

關於變更標記有一個小例外。

我們可以將不可設定屬性的 `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

這些方法在實務上很少使用。

教學課程地圖

留言

留言前請先閱讀…
  • 如果你有建議要如何改進 - 請 提交 GitHub 議題 或發起 pull request,而不是留言。
  • 如果你看不懂文章中的某個部分 - 請詳細說明。
  • 要插入少量的程式碼,請使用 <code> 標籤,要插入多行,請用 <pre> 標籤包起來,要插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)