根據規格,只有兩種原始類型可以作為物件屬性金鑰
- 字串類型,或
- 符號類型。
否則,如果使用其他類型,例如數字,它會自動轉換為字串。因此 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,每個名稱只有一個符號。
在 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
會傳回完全相同的符號。
符號有兩個主要的用例
-
「隱藏的」物件屬性。
如果我們想要將一個「屬於」其他腳本或函式庫的屬性新增到物件中,我們可以建立一個符號並將它用作屬性鍵。符號屬性不會出現在
for..in
中,因此它不會與其他屬性一起意外處理。此外,它也不會被直接存取,因為其他腳本沒有我們的符號。因此,該屬性將受到意外使用或覆寫的保護。因此,我們可以使用符號屬性「秘密地」將某些東西隱藏到我們需要的物件中,但其他人不應該看到。
-
JavaScript 使用許多系統符號,可以透過
Symbol.*
存取。我們可以使用它們來改變一些內建行為。例如,稍後在教學課程中,我們將使用Symbol.iterator
來處理 可迭代 項目,使用Symbol.toPrimitive
來設定 物件到原始值的轉換,等等。
技術上來說,符號並非 100% 隱藏。有一個內建方法 Object.getOwnPropertySymbols(obj) 允許我們取得所有符號。此外,還有一個名為 Reflect.ownKeys(obj) 的方法,它傳回物件的所有鍵,包括符號鍵。但大多數函式庫、內建函式和語法結構都不會使用這些方法。
留言
<code>
標籤;若要插入多行程式碼,請使用<pre>
標籤;若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)