2022 年 6 月 27 日

符號類型

根據規格,只有兩種原始類型可以作為物件屬性金鑰

  • 字串類型,或
  • 符號類型。

否則,如果使用其他類型,例如數字,它會自動轉換為字串。因此 obj[1]obj["1"] 相同,而 obj[true]obj["true"] 相同。

到目前為止,我們只使用過字串。

現在讓我們來探討符號,看看它們能為我們做些什麼。

符號

「符號」代表一個唯一的識別碼。

可以使用 Symbol() 來建立這種類型的值。

let id = Symbol();

在建立後,我們可以為符號提供一個描述(也稱為符號名稱),這在除錯時特別有用。

// id is a symbol with the description "id"
let id = Symbol("id");

符號保證是唯一的。即使我們建立許多具有完全相同描述的符號,它們也是不同的值。描述只是一個標籤,不會影響任何事情。

例如,這裡有兩個具有相同描述的符號,它們不相等

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

如果你熟悉 Ruby 或其他也具有某種「符號」的語言,請不要誤解。JavaScript 符號是不同的。

因此,總之,符號是一個具有可選描述的「原始唯一值」。讓我們看看我們可以在哪裡使用它們。

符號不會自動轉換為字串

JavaScript 中的大多數值都支援隱式轉換為字串。例如,我們幾乎可以對任何值使用 alert,它都會正常運作。符號很特別。它們不會自動轉換。

例如,這個 alert 會顯示一個錯誤

let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string

這是一個防止搞亂的「語言防護」,因為字串和符號在根本上是不同的,不應該意外地將一個轉換為另一個。

如果我們真的想顯示一個符號,我們需要明確地在它上面呼叫 .toString(),如下所示

let id = Symbol("id");
alert(id.toString()); // Symbol(id), now it works

或取得 symbol.description 屬性來僅顯示描述

let id = Symbol("id");
alert(id.description); // id

「隱藏」屬性

符號允許我們建立一個物件的「隱藏」屬性,程式碼的其他部分無法意外地存取或覆寫它。

例如,如果我們正在處理屬於第三方程式碼的 user 物件。我們想為它們新增識別碼。

讓我們為它使用一個符號鍵

let user = { // belongs to another code
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // we can access the data using the symbol as the key

使用 Symbol("id") 比使用字串 "id" 有什麼好處?

由於 user 物件屬於另一個程式碼庫,因此在它們上面新增欄位是不安全的,因為我們可能會影響該另一個程式碼庫中預先定義的行為。但是,無法意外地存取符號。第三方程式碼不會知道新定義的符號,因此將符號新增到 user 物件是安全的。

此外,想像另一個腳本想要在 user 內部有自己的識別碼,以供其自己使用。

然後,該腳本可以建立自己的 Symbol("id"),如下所示

// ...
let id = Symbol("id");

user[id] = "Their id value";

我們的識別碼和他們的識別碼之間不會有衝突,因為符號總是不同的,即使它們具有相同的名稱。

…但是,如果我們為相同目的使用字串 "id" 而不是符號,那麼就會有衝突

let user = { name: "John" };

// Our script uses "id" property
user.id = "Our id value";

// ...Another script also wants "id" for its purposes...

user.id = "Their id value"
// Boom! overwritten by another script!

物件文字中的符號

如果我們要在物件文字 {...} 中使用符號,我們需要在它周圍加上方括號。

像這樣

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // not "id": 123
};

這是因為我們需要變數 id 的值作為鍵,而不是字串「id」。

for…in 會略過符號

符號屬性不會參與 for..in 迴圈。

例如

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name, age (no symbols)

// the direct access by the symbol works
alert( "Direct: " + user[id] ); // Direct: 123

Object.keys(user) 也會忽略它們。這是「隱藏符號屬性」原則的一部分。如果另一個腳本或程式庫迴圈我們的物件,它不會意外存取符號屬性。

相反地,Object.assign 會複製字串和符號屬性

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assign({}, user);

alert( clone[id] ); // 123

這裡沒有矛盾。這是設計使然。這個想法是,當我們複製物件或合併物件時,我們通常希望複製所有屬性(包括 id 等符號)。

全域符號

如我們所見,通常所有符號都不同,即使它們有相同的名稱。但有時我們希望同名的符號是同一個實體。例如,我們應用程式的不同部分希望存取符號 "id",表示完全相同的屬性。

為了達成這個目標,存在一個全域符號註冊表。我們可以在其中建立符號並稍後存取它們,它保證以相同的名稱重複存取會傳回完全相同的符號。

為了從註冊表中讀取(如果不存在則建立)符號,請使用 Symbol.for(key)

該呼叫會檢查全域註冊表,如果有一個符號描述為 key,則傳回它,否則建立一個新的符號 Symbol(key) 並將它儲存在註冊表中,其鍵為給定的 key

例如

// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created

// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");

// the same symbol
alert( id === idAgain ); // true

註冊表中的符號稱為全域符號。如果我們想要一個應用程式範圍的符號,可以在程式碼中的任何地方存取,這就是它們的用途。

這聽起來像 Ruby

在某些程式語言中,例如 Ruby,每個名稱只有一個符號。

在 JavaScript 中,正如我們所見,這對全域符號來說是正確的。

Symbol.keyFor

我們已經看到,對於全域符號,Symbol.for(key) 會傳回一個符號,其名稱為 key。要執行相反的動作,即傳回全域符號的名稱,我們可以使用:Symbol.keyFor(sym)

例如

// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// get name by symbol
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor 在內部使用全域符號註冊表來查詢符號的鍵。因此它不適用於非全域符號。如果符號不是全域的,它將無法找到它並傳回 undefined

話雖如此,所有符號都有 description 屬性。

例如

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // name, global symbol
alert( Symbol.keyFor(localSymbol) ); // undefined, not global

alert( localSymbol.description ); // name

系統符號

存在許多 JavaScript 在內部使用的「系統」符號,我們可以使用它們來微調物件的各個方面。

它們在規格中的 知名符號 表格中列出

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • …等等。

例如,Symbol.toPrimitive 允許我們描述物件到原始值的轉換。我們很快就會看到它的用法。

當我們學習對應的語言功能時,其他符號也會變得熟悉。

摘要

Symbol 是用於唯一識別碼的原始類型。

符號使用 Symbol() 呼叫建立,並附有選用的描述 (名稱)。

符號永遠是不同的值,即使它們有相同的名稱。如果我們希望同名的符號相等,那麼我們應該使用全域註冊表:Symbol.for(key) 傳回 (如果需要,會建立) 一個以 key 為名稱的全域符號。使用相同的 key 多次呼叫 Symbol.for 會傳回完全相同的符號。

符號有兩個主要的用例

  1. 「隱藏的」物件屬性。

    如果我們想要將一個「屬於」其他腳本或函式庫的屬性新增到物件中,我們可以建立一個符號並將它用作屬性鍵。符號屬性不會出現在 for..in 中,因此它不會與其他屬性一起意外處理。此外,它也不會被直接存取,因為其他腳本沒有我們的符號。因此,該屬性將受到意外使用或覆寫的保護。

    因此,我們可以使用符號屬性「秘密地」將某些東西隱藏到我們需要的物件中,但其他人不應該看到。

  2. JavaScript 使用許多系統符號,可以透過 Symbol.* 存取。我們可以使用它們來改變一些內建行為。例如,稍後在教學課程中,我們將使用 Symbol.iterator 來處理 可迭代 項目,使用 Symbol.toPrimitive 來設定 物件到原始值的轉換,等等。

技術上來說,符號並非 100% 隱藏。有一個內建方法 Object.getOwnPropertySymbols(obj) 允許我們取得所有符號。此外,還有一個名為 Reflect.ownKeys(obj) 的方法,它傳回物件的所有鍵,包括符號鍵。但大多數函式庫、內建函式和語法結構都不會使用這些方法。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請提交 GitHub 議題或提出 Pull Request,而非留言。
  • 如果您看不懂文章中的某個部分,請說明。
  • 若要插入少許程式碼,請使用 <code> 標籤;若要插入多行程式碼,請使用 <pre> 標籤;若要插入超過 10 行程式碼,請使用沙盒 (plnkrjsbincodepen…)