Javascript symbol 類型

2023-02-17 10:44 更新

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

  • 字符串類型
  • symbol 類型

否則,如果使用另一種類型,例如數(shù)字,它會被自動(dòng)轉(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)建時(shí),我們可以給 symbol 一個(gè)描述(也稱為 symbol 名),這在代碼調(diào)試時(shí)非常有用:

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

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

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

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

alert(id1 == id2); // false

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

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

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

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

例如,這個(gè) alert 將會提示出錯(cuò):

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

這是一種防止混亂的“語言保護(hù)”,因?yàn)樽址?symbol 有本質(zhì)上的不同,不應(yīng)該意外地將它們轉(zhuǎn)換成另一個(gè)。

如果我們真的想顯示一個(gè) 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 = { // 屬于另一個(gè)代碼
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

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

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

由于 user 對象屬于另一個(gè)代碼庫,所以向它們添加字段是不安全的,因?yàn)槲覀兛赡軙绊懘a庫中的其他預(yù)定義行為。但 symbol 屬性不會被意外訪問到。第三方代碼不會知道新定義的 symbol,因此將 symbol 添加到 user 對象是安全的。

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

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

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

user[id] = "Their id value";

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

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

let user = { name: "John" };

// 我們的腳本使用了 "id" 屬性。
user.id = "Our id value";

// ……另一個(gè)腳本也想將 "id" 用于它的目的……

user.id = "Their id value"
// 砰!無意中被另一個(gè)腳本重寫了 id!

對象字面量中的 symbol

如果我們要在對象字面量 {...} 中使用 symbol,則需要使用方括號把它括起來。

就像這樣:

let id = Symbol("id");

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

這是因?yàn)槲覀冃枰兞?nbsp;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) 也會忽略它們。這是一般“隱藏符號屬性”原則的一部分。如果另一個(gè)腳本或庫遍歷我們的對象,它不會意外地訪問到符號屬性。

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

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

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

alert( clone[id] ); // 123

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

全局 symbol

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

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

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

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

例如:

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

// 再次讀?。赡苁窃诖a中的另一個(gè)位置)
let idAgain = Symbol.for("id");

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

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

這聽起來像 Ruby

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

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

Symbol.keyFor

我們已經(jīng)看到,對于全局 symbol,Symbol.for(key) 按名字返回一個(gè) symbol。相反,通過全局 symbol 返回一個(gè)名字,我們可以使用 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)對象的各個(gè)方面。

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

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

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

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

總結(jié)

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

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

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

symbol 有兩個(gè)主要的使用場景:

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號