根據規範,只有兩種原始類型可以用作對象屬性鍵:
字符串類型
symbol 類型
否則,如果使用另壹種類型,例如數字,它會被自動轉換爲字符串。所以 obj[1]
與 obj["1"]
相同,而 obj[true]
與 obj["true"]
相同。
到目前爲止,我們壹直只使用字符串。
現在我們來看看 symbol 能給我們帶來什麽。
“symbol” 值表示唯壹的標識符。
可以使用 Symbol()
來創建這種類型的值:
let id = Symbol();
創建時,我們可以給 symbol 壹個描述(也稱爲 symbol 名),這在代碼調試時非常有用:
// id 是描述爲 "id" 的 symbol let id = Symbol("id");
symbol 保證是唯壹的。即使我們創建了許多具有相同描述的 symbol,它們的值也是不同。描述只是壹個標簽,不影響任何東西。
例如,這裏有兩個描述相同的 symbol —— 它們不相等:
let id1 = Symbol("id"); let id2 = Symbol("id"); alert(id1 == id2); // false
如果妳熟悉 Ruby 或者其他有 “symbol” 的語言 —— 別被誤導。JavaScript 的 symbol 是不同的。
所以,總而言之,symbol 是帶有可選描述的“原始唯壹值”。讓我們看看我們可以在哪裏使用它們。
symbol 不會被自動轉換爲字符串
JavaScript 中的大多數值都支持字符串的隱式轉換。例如,我們可以 alert
任何值,都可以生效。symbol 比較特殊,它不會被自動轉換。
例如,這個 alert
將會提示出錯:
let id = Symbol("id"); alert(id); // 類型錯誤:無法將 symbol 值轉換爲字符串。
這是壹種防止混亂的“語言保護”,因爲字符串和 symbol 有本質上的不同,不應該意外地將它們轉換成另壹個。
如果我們真的想顯示壹個 symbol,我們需要在它上面調用 .toString()
,如下所示:
let id = Symbol("id"); alert(id.toString()); // Symbol(id),現在它有效了
或者獲取 symbol.description
屬性,只顯示描述(description):
let id = Symbol("id"); alert(id.description); // id
symbol 允許我們創建對象的“隱藏”屬性,代碼的任何其他部分都不能意外訪問或重寫這些屬性。
例如,如果我們使用的是屬于第三方代碼的 user
對象,我們想要給它們添加壹些標識符。
我們可以給它們使用 symbol 鍵:
let user = { // 屬于另壹個代碼 name: "John" }; let id = Symbol("id"); user[id] = 1; alert( user[id] ); // 我們可以使用 symbol 作爲鍵來訪問數據
使用 Symbol("id")
作爲鍵,比起用字符串 "id"
來有什麽好處呢?
由于 user
對象屬于另壹個代碼庫,所以向它們添加字段是不安全的,因爲我們可能會影響代碼庫中的其他預定義行爲。但 symbol 屬性不會被意外訪問到。第三方代碼不會知道新定義的 symbol,因此將 symbol 添加到 user
對象是安全的。
另外,假設另壹個腳本希望在 user
中有自己的標識符,以實現自己的目的。
那麽,該腳本可以創建自己的 Symbol("id")
,像這樣:
// ... let id = Symbol("id"); user[id] = "Their id value";
我們的標識符和它們的標識符之間不會有沖突,因爲 symbol 總是不同的,即使它們有相同的名字。
……但如果我們處于同樣的目的,使用字符串 "id"
而不是用 symbol,那麽 就會 出現沖突:
let user = { name: "John" }; // 我們的腳本使用了 "id" 屬性。 user.id = "Our id value"; // ……另壹個腳本也想將 "id" 用于它的目的…… user.id = "Their id value" // 砰!無意中被另壹個腳本重寫了 id!
如果我們要在對象字面量 {...}
中使用 symbol,則需要使用方括號把它括起來。
就像這樣:
let id = Symbol("id"); let user = { name: "John", [id]: 123 // 而不是 "id":123 };
這是因爲我們需要變量 id
的值作爲鍵,而不是字符串 “id”。
symbol 屬性不參與 for..in
循環。
例如:
let id = Symbol("id"); let user = { name: "John", age: 30, [id]: 123 }; for (let key in user) alert(key); // name, age(沒有 symbol) // 使用 symbol 任務直接訪問 alert("Direct: " + user[id]); // Direct: 123
Object.keys(user) 也會忽略它們。這是壹般“隱藏符號屬性”原則的壹部分。如果另壹個腳本或庫遍曆我們的對象,它不會意外地訪問到符號屬性。
相反,Object.assign 會同時複制字符串和 symbol 屬性:
let id = Symbol("id"); let user = { [id]: 123 }; let clone = Object.assign({}, user); alert( clone[id] ); // 123
這裏並不矛盾,就是這樣設計的。這裏的想法是當我們克隆或者合並壹個 object 時,通常希望 所有 屬性被複制(包括像 id
這樣的 symbol)。
正如我們所看到的,通常所有的 symbol 都是不同的,即使它們有相同的名字。但有時我們想要名字相同的 symbol 具有相同的實體。例如,應用程序的不同部分想要訪問的 symbol "id"
指的是完全相同的屬性。
爲了實現這壹點,這裏有壹個 全局 symbol 注冊表。我們可以在其中創建 symbol 並在稍後訪問它們,它可以確保每次訪問相同名字的 symbol 時,返回的都是相同的 symbol。
要從注冊表中讀取(不存在則創建)symbol,請使用 Symbol.for(key)
。
該調用會檢查全局注冊表,如果有壹個描述爲 key
的 symbol,則返回該 symbol,否則將創建壹個新 symbol(Symbol(key)
),並通過給定的 key
將其存儲在注冊表中。
例如:
// 從全局注冊表中讀取 let id = Symbol.for("id"); // 如果該 symbol 不存在,則創建它 // 再次讀取(可能是在代碼中的另壹個位置) let idAgain = Symbol.for("id"); // 相同的 symbol alert( id === idAgain ); // true
注冊表內的 symbol 被稱爲 全局 symbol。如果我們想要壹個應用程序範圍內的 symbol,可以在代碼中隨處訪問 —— 這就是它們的用途。
這聽起來像 Ruby
在壹些編程語言中,例如 Ruby,每個名字都有壹個 symbol。
正如我們所看到的,在 JavaScript 中,全局 symbol 也是這樣的。
我們已經看到,對于全局 symbol,Symbol.for(key)
按名字返回壹個 symbol。相反,通過全局 symbol 返回壹個名字,我們可以使用 Symbol.keyFor(sym)
:
例如:
// 通過 name 獲取 symbol let sym = Symbol.for("name"); let sym2 = Symbol.for("id"); // 通過 symbol 獲取 name alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor
內部使用全局 symbol 注冊表來查找 symbol 的鍵。所以它不適用于非全局 symbol。如果 symbol 不是全局的,它將無法找到它並返回 undefined
。
但是,所有 symbol 都具有 description
屬性。
例如:
let globalSymbol = Symbol.for("name"); let localSymbol = Symbol("name"); alert( Symbol.keyFor(globalSymbol) ); // name,全局 symbol alert( Symbol.keyFor(localSymbol) ); // undefined,非全局 alert( localSymbol.description ); // name
JavaScript 內部有很多“系統” symbol,我們可以使用它們來微調對象的各個方面。
它們都被列在了 衆所周知的 symbol 表的規範中:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
……等等。
例如,Symbol.toPrimitive
允許我們將對象描述爲原始值轉換。我們很快就會看到它的使用。
當我們研究相應的語言特征時,我們對其他的 symbol 也會慢慢熟悉起來。
symbol
是唯壹標識符的基本類型
symbol 是使用帶有可選描述(name)的 Symbol()
調用創建的。
symbol 總是不同的值,即使它們有相同的名字。如果我們希望同名的 symbol 相等,那麽我們應該使用全局注冊表:Symbol.for(key)
返回(如果需要的話則創建)壹個以 key
作爲名字的全局 symbol。使用 Symbol.for
多次調用 key
相同的 symbol 時,返回的就是同壹個 symbol。
symbol 有兩個主要的使用場景:
“隱藏” 對象屬性。
如果我們想要向“屬于”另壹個腳本或者庫的對象添加壹個屬性,我們可以創建壹個 symbol 並使用它作爲屬性的鍵。symbol 屬性不會出現在 for..in
中,因此它不會意外地被與其他屬性壹起處理。並且,它不會被直接訪問,因爲另壹個腳本沒有我們的 symbol。因此,該屬性將受到保護,防止被意外使用或重寫。
因此我們可以使用 symbol 屬性“秘密地”將壹些東西隱藏到我們需要的對象中,但其他地方看不到它。
JavaScript 使用了許多系統 symbol,這些 symbol 可以作爲 Symbol.*
訪問。我們可以使用它們來改變壹些內建行爲。例如,在本教程的後面部分,我們將使用 Symbol.iterator
來進行 叠代 操作,使用 Symbol.toPrimitive
來設置 對象原始值的轉換 等等。
從技術上說,symbol 不是 100% 隱藏的。有壹個內建方法 Object.getOwnPropertySymbols(obj) 允許我們獲取所有的 symbol。還有壹個名爲 Reflect.ownKeys(obj) 的方法可以返回壹個對象的 所有 鍵,包括 symbol。但大多數庫、內建方法和語法結構都沒有使用這些方法。