Javascript symbol 類型

2023-02-17 10:44 更新

根據(jù)規(guī)范,只有兩種原始類型可以用作對象屬性鍵:

  • 字符串類型
  • symbol 類型

否則,如果使用另一種類型,例如數(shù)字,它會被自動轉(zhuǎn)換為字符串。所以 obj[1] 與 obj["1"] 相同,而 obj[true] 與 obj["true"] 相同。

到目前為止,我們一直只使用字符串。

現(xiàn)在我們來看看 symbol 能給我們帶來什么。

symbol

“symbol” 值表示唯一的標(biāo)識符。

可以使用 Symbol() 來創(chuàng)建這種類型的值:

let id = Symbol();

創(chuàng)建時,我們可以給 symbol 一個描述(也稱為 symbol 名),這在代碼調(diào)試時非常有用:

// id 是描述為 "id" 的 symbol
let id = Symbol("id");

symbol 保證是唯一的。即使我們創(chuàng)建了許多具有相同描述的 symbol,它們的值也是不同。描述只是一個標(biāo)簽,不影響任何東西。

例如,這里有兩個描述相同的 symbol —— 它們不相等:

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

alert(id1 == id2); // false

如果你熟悉 Ruby 或者其他有 “symbol” 的語言 —— 別被誤導(dǎo)。JavaScript 的 symbol 是不同的。

所以,總而言之,symbol 是帶有可選描述的“原始唯一值”。讓我們看看我們可以在哪里使用它們。

symbol 不會被自動轉(zhuǎn)換為字符串

JavaScript 中的大多數(shù)值都支持字符串的隱式轉(zhuǎn)換。例如,我們可以 alert 任何值,都可以生效。symbol 比較特殊,它不會被自動轉(zhuǎn)換。

例如,這個 alert 將會提示出錯:

let id = Symbol("id");
alert(id); // 類型錯誤:無法將 symbol 值轉(zhuǎn)換為字符串。

這是一種防止混亂的“語言保護”,因為字符串和 symbol 有本質(zhì)上的不同,不應(yīng)該意外地將它們轉(zhuǎn)換成另一個。

如果我們真的想顯示一個 symbol,我們需要在它上面調(diào)用 .toString(),如下所示:

let id = Symbol("id");
alert(id.toString()); // Symbol(id),現(xiàn)在它有效了

或者獲取 symbol.description 屬性,只顯示描述(description):

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

“隱藏”屬性

symbol 允許我們創(chuàng)建對象的“隱藏”屬性,代碼的任何其他部分都不能意外訪問或重寫這些屬性。

例如,如果我們使用的是屬于第三方代碼的 user 對象,我們想要給它們添加一些標(biāo)識符。

我們可以給它們使用 symbol 鍵:

let user = { // 屬于另一個代碼
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // 我們可以使用 symbol 作為鍵來訪問數(shù)據(jù)

使用 Symbol("id") 作為鍵,比起用字符串 "id" 來有什么好處呢?

由于 user 對象屬于另一個代碼庫,所以向它們添加字段是不安全的,因為我們可能會影響代碼庫中的其他預(yù)定義行為。但 symbol 屬性不會被意外訪問到。第三方代碼不會知道新定義的 symbol,因此將 symbol 添加到 user 對象是安全的。

另外,假設(shè)另一個腳本希望在 user 中有自己的標(biāo)識符,以實現(xiàn)自己的目的。

那么,該腳本可以創(chuàng)建自己的 Symbol("id"),像這樣:

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

user[id] = "Their id value";

我們的標(biāo)識符和它們的標(biāo)識符之間不會有沖突,因為 symbol 總是不同的,即使它們有相同的名字。

……但如果我們處于同樣的目的,使用字符串 "id" 而不是用 symbol,那么 就會 出現(xiàn)沖突:

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 中會被跳過

symbol 屬性不參與 for..in 循環(huán)。

例如:

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

for (let key in user) alert(key); // name, age(沒有 symbol)

// 使用 symbol 任務(wù)直接訪問
alert("Direct: " + user[id]); // Direct: 123

Object.keys(user) 也會忽略它們。這是一般“隱藏符號屬性”原則的一部分。如果另一個腳本或庫遍歷我們的對象,它不會意外地訪問到符號屬性。

相反,Object.assign 會同時復(fù)制字符串和 symbol 屬性:

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

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

alert( clone[id] ); // 123

這里并不矛盾,就是這樣設(shè)計的。這里的想法是當(dāng)我們克隆或者合并一個 object 時,通常希望 所有 屬性被復(fù)制(包括像 id 這樣的 symbol)。

全局 symbol

正如我們所看到的,通常所有的 symbol 都是不同的,即使它們有相同的名字。但有時我們想要名字相同的 symbol 具有相同的實體。例如,應(yīng)用程序的不同部分想要訪問的 symbol "id" 指的是完全相同的屬性。

為了實現(xiàn)這一點,這里有一個 全局 symbol 注冊表。我們可以在其中創(chuàng)建 symbol 并在稍后訪問它們,它可以確保每次訪問相同名字的 symbol 時,返回的都是相同的 symbol。

要從注冊表中讀?。ú淮嬖趧t創(chuàng)建)symbol,請使用 Symbol.for(key)。

該調(diào)用會檢查全局注冊表,如果有一個描述為 key 的 symbol,則返回該 symbol,否則將創(chuàng)建一個新 symbol(Symbol(key)),并通過給定的 key 將其存儲在注冊表中。

例如:

// 從全局注冊表中讀取
let id = Symbol.for("id"); // 如果該 symbol 不存在,則創(chuàng)建它

// 再次讀取(可能是在代碼中的另一個位置)
let idAgain = Symbol.for("id");

// 相同的 symbol
alert( id === idAgain ); // true

注冊表內(nèi)的 symbol 被稱為 全局 symbol。如果我們想要一個應(yīng)用程序范圍內(nèi)的 symbol,可以在代碼中隨處訪問 —— 這就是它們的用途。

這聽起來像 Ruby

在一些編程語言中,例如 Ruby,每個名字都有一個 symbol。

正如我們所看到的,在 JavaScript 中,全局 symbol 也是這樣的。

Symbol.keyFor

我們已經(jīng)看到,對于全局 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 內(nèi)部使用全局 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

系統(tǒng) symbol

JavaScript 內(nèi)部有很多“系統(tǒng)” symbol,我們可以使用它們來微調(diào)對象的各個方面。

它們都被列在了 眾所周知的 symbol 表的規(guī)范中:

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

例如,Symbol.toPrimitive 允許我們將對象描述為原始值轉(zhuǎn)換。我們很快就會看到它的使用。

當(dāng)我們研究相應(yīng)的語言特征時,我們對其他的 symbol 也會慢慢熟悉起來。

總結(jié)

symbol 是唯一標(biāo)識符的基本類型

symbol 是使用帶有可選描述(name)的 Symbol() 調(diào)用創(chuàng)建的。

symbol 總是不同的值,即使它們有相同的名字。如果我們希望同名的 symbol 相等,那么我們應(yīng)該使用全局注冊表:Symbol.for(key) 返回(如果需要的話則創(chuàng)建)一個以 key 作為名字的全局 symbol。使用 Symbol.for 多次調(diào)用 key 相同的 symbol 時,返回的就是同一個 symbol。

symbol 有兩個主要的使用場景:

  1. “隱藏” 對象屬性。
  2. 如果我們想要向“屬于”另一個腳本或者庫的對象添加一個屬性,我們可以創(chuàng)建一個 symbol 并使用它作為屬性的鍵。symbol 屬性不會出現(xiàn)在 for..in 中,因此它不會意外地被與其他屬性一起處理。并且,它不會被直接訪問,因為另一個腳本沒有我們的 symbol。因此,該屬性將受到保護,防止被意外使用或重寫。

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

  3. JavaScript 使用了許多系統(tǒng) symbol,這些 symbol 可以作為 ?Symbol.*? 訪問。我們可以使用它們來改變一些內(nèi)建行為。例如,在本教程的后面部分,我們將使用 ?Symbol.iterator? 來進行 迭代 操作,使用 ?Symbol.toPrimitive? 來設(shè)置 對象原始值的轉(zhuǎn)換 等等。

從技術(shù)上說,symbol 不是 100% 隱藏的。有一個內(nèi)建方法 Object.getOwnPropertySymbols(obj) 允許我們獲取所有的 symbol。還有一個名為 Reflect.ownKeys(obj) 的方法可以返回一個對象的 所有 鍵,包括 symbol。但大多數(shù)庫、內(nèi)建方法和語法結(jié)構(gòu)都沒有使用這些方法。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號