你知道 JavaScript Symbol 類型是什麼,怎麼用嗎?

Symbol 類型

根據規範,對象的屬性鍵只能是字符串類型或者 Symbol 類型。不是 Number,也不是 Boolean,只有字符串或 Symbol 這兩種類型。javascript

到目前爲止,咱們只見過字符串。如今咱們來看看 Symbol 能給咱們帶來什麼好處。html

Symbol

"Symbol" 值表示惟一的標識符。java

可使用 Symbol() 來建立這種類型的值:react

// id 是 symbol 的一個實例化對象
let id = Symbol();
複製代碼

建立時,咱們能夠給 Symbol 一個描述(也稱爲 Symbol 名),這在代碼調試時很是有用:git

// id 是描述爲 "id" 的 Symbol
let id = Symbol("id");
複製代碼

Symbol 保證是惟一的。即便咱們建立了許多具備相同描述的 Symbol,它們的值也是不一樣。描述只是一個標籤,不影響任何東西。github

例如,這裏有兩個描述相同的 Symbol —— 它們不相等:編程

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

alert(id1 == id2); // false
複製代碼

若是你熟悉 Ruby 或者其餘有 "Symbol" 的語言 —— 別被誤導。JavaScript 的 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 做爲鍵來訪問數據
複製代碼

在字符串 "id" 上使用 Symbol("id") 有什麼好處?

由於 user 屬於另外一個代碼,另外一個代碼也使用它執行一些操做,因此咱們不該該在它上面加任何字段,這樣很不安全。可是 Symbol 不會被意外訪問到,因此第三方代碼看不到它,因此使用 Symbol 也許不會有什麼問題。

另外,假設另外一個腳本但願在 user 中有本身的標識符,以實現本身的目的。這多是另外一個 JavaScript 庫,所以腳本之間徹底不瞭解彼此。

而後該腳本能夠建立本身的 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

若是咱們要在對象字面量 {...} 中使用 Symbol,則須要使用方括號把它括起來。

就像這樣:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // 而不是 "id:123"
};
複製代碼

這是由於咱們須要變量 id 的值做爲鍵,而不是字符串 "id"。

Symbol 在 for..in 中會被跳過

Symbolic 屬性不參與 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)

// 使用 Symbol 任務直接訪問
alert( "Direct: " + user[id] );
複製代碼

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 做爲鍵,其它類型會被轉換爲字符串。

例如,在做爲屬性鍵使用時,數字 0 變成了字符串 "0"

let obj = {
  0: "test" // 和 "0": "test" 同樣
};

// 兩個 alert 都訪問相同的屬性(Number 0 被轉換爲字符串 "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test(同一個屬性)
複製代碼

全局 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.keyFor

對於全局 Symbol,不只有 Symbol.for(key) 按名字返回一個 Symbol,還有一個反向調用:Symbol.keyFor(sym),它的做用徹底反過來:經過全局 Symbol 返回一個名字。

例如:

// 經過 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
複製代碼

系統 Symbol

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 有兩個主要的使用場景:

  1. 「隱藏」 對象屬性。 若是咱們想要向「屬於」另外一個腳本或者庫的對象添加一個屬性,咱們能夠建立一個 Symbol 並使用它做爲屬性的鍵。Symbol 屬性不會出如今 for..in 中,所以它不會意外地被與其餘屬性一塊兒處理。而且,它不會被直接訪問,由於另外一個腳本沒有咱們的 symbol。所以,該屬性將受到保護,防止被意外使用或重寫。

    所以咱們可使用 Symbol 屬性「祕密地」將一些東西隱藏到咱們須要的對象中,但其餘地方看不到它。

  2. JavaScript 使用了許多系統 Symbol,這些 Symbol 能夠做爲 Symbol.* 訪問。咱們可使用它們來改變一些內置行爲。例如,在本教程的後面部分,咱們將使用 Symbol.iterator 來進行 迭代 操做,使用 Symbol.toPrimitive 來設置 對象原始值的轉換 等等。

從技術上說,Symbol 不是 100% 隱藏的。有一個內置方法 Object.getOwnPropertySymbols(obj) 容許咱們獲取全部的 Symbol。還有一個名爲 Reflect.ownKeys(obj) 的方法能夠返回一個對象的 全部 鍵,包括 Symbol。因此它們並非真正的隱藏。可是大多數庫、內置方法和語法結構都沒有使用這些方法。

本文首發於微信公衆號「技術漫談」,歡迎微信搜索關注,訂閱更多精彩內容。


現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文檔推薦,與 MDN 並列的 JavaScript 學習教程

在線免費閱讀:zh.javascript.info


掃描下方二維碼,關注微信公衆號「技術漫談」,訂閱更多精彩內容。

相關文章
相關標籤/搜索