1. 程式人生 > 程式設計 >你知道JavaScript Symbol型別怎麼用嗎

你知道JavaScript Symbol型別怎麼用嗎

Symbol 型別

根據規範,物件的屬性鍵只能是字串型別或者 Symbol 型別。不是 Number,也不是 Boolean,只有字串或 Symbol 這兩種型別。

到目前為止,我們只見過字串。現在我們來看看 Symbol 能給我們帶來什麼好處。

Symbol

"Symbol" 值表示唯一的識別符號。

可以使用 Symbol() 來建立這種型別的值:

// id 是 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 不會被自動轉換為字串

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

  • “隱藏” 物件屬性。如果我們想要向“屬於”另一個指令碼或者庫的物件新增一個屬性,我們可以建立一個 Symbol 並使用它作為屬性的鍵。Symbol 屬性不會出現在 for..in 中,因此它不會意外地被與其他屬性一起處理。並且,它不會被直接訪問,因為另一個指令碼沒有我們的 symbol。因此,該屬性將受到保護,防止被意外使用或重寫。

因此我們可以使用 Symbol 屬性“祕密地”將一些東西隱藏到我們需要的物件中,但其他地方看不到它。

  • JavaScript 使用了許多系統 Symbol,這些 Symbol 可以作為 Symbol.* 訪問。我們可以使用它們來改變一些內建行為。例如,在本教程的後面部分,我們將使用 Symbol.iterator 來進行 迭代 操作,使用 Symbol.toPrimitive 來設定 物件原始值的轉換 等等。

從技術上說,Symbol 不是 100% 隱藏的。有一個內建方法Object.getOwnPropertySymbols(obj) 允許我們獲取所有的 Symbol。還有一個名為 Reflect.ownKeys(obj) 的方法可以返回一個物件的 所有 鍵,包括 Symbol。所以它們並不是真正的隱藏。但是大多數庫、內建方法和語法結構都沒有使用這些方法。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。