ES6 Symbol之淺入解讀?

SaltAir發表於2018-12-16

一、介紹

Symbol是ES6新增的資料型別,是一種基礎資料型別,MDN 中的對Symbol型別的描述為:資料型別 “symbol” 是一種原始資料型別,該型別的性質在於這個型別的值可以用來建立匿名的物件屬性。該資料型別通常被用作一個物件屬性的鍵值——當你想讓它是私有的時候。


symbol 資料型別具有非常明確的目的,並且因為其功能性單一的優點而突出;一個 symbol 例項可以被賦值到一個左值變數,還可以通過識別符號檢查型別,這就是它的全部特性。

這些描述幾乎完全說明了這個新來的資料型別的用途:作為一個物件或一個Map的鍵值,他可以保證你的物件或Map的鍵值不重複(這個在某些場景下真的非常有用)。 這個資料型別因為是ES6新增的,所以不存在polyfill。

二、用法

1. 物件

1.1 普通用法

能夠用來建立Symbol的是一個像類的函式 Symbol(),用來建立 symbol 資料型別例項。注意,Symbol函式前不能使用new命令,否則會報錯。這是因為生成的 Symbol 是一個原始型別的值,不是物件。也就是說,由於 Symbol 值不是物件,所以不能新增屬性。基本上,它是一種類似於字串的資料型別。【此處引用阮一峰老師在ES6教程裡面的話】MDN上的解釋是:一個具有資料型別 “symbol” 的值可以被稱為 “符號型別值”。在 JavaScript 執行時環境中,一個符號型別值可以通過呼叫函式 Symbol() 建立,這個函式動態地生成了一個匿名,唯一的值。Symbol型別唯一合理的用法是用變數儲存 symbol的值,然後使用儲存的值建立物件屬性。 最簡單的作為物件的key的寫法如下:

var  privateKey  = Symbol();
var obj = {
    [privateKey] : 'hero'
}
//訪問時
obj[privateKey]  //hero
複製程式碼

一般在鍵值對物件中我們訪問某個屬性的時候常用.符號來取到對應的值,但是用.符號去取值時,.後面一定是一個字串,因為ES6之前物件的key必須是字串,所以當訪問以Symbol例項為key時需要使用[]來包裹起來 (ES6同時增加了兩種資料結構Set和Map,Map在某種意義上來講,更加適合用來儲存鍵值對的形式的資料,可以參照Java的Map的資料結構)。

var identity = Symbol()
var obj = {
    name : 'john',
    [identity] : 'hero'
}
obj.name   //'john'
obj[name]  //'john'
obj.identity   //undefined
//當我們使用.來訪問時,因為不存在這個key,所以就會返回undefined,也符合上方所寫的
obj[identity]  //'hero'
複製程式碼
1.2 遍歷含有Symbol的物件

但是我們在遍歷某個物件時,使用for in或for of方法時,Symbol為key或value時是不會出現在遍歷結果裡的;同樣也不會被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但是伴隨著Symbol一起出現的有一個方法可以取到它的值:Object.getOwnPropertySymbols,可以獲取指定物件的所有 Symbol 屬性名。 Object.getOwnPropertySymbols方法返回一個陣列,成員是當前物件的所有用作屬性名的 Symbol 值。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]
複製程式碼

但是我們還有一個API可以用來將一個物件裡面所有的鍵值全部反映出來,包含普通鍵和Symbol鍵:Reflect.ownKeys。【參考自阮一峰的ES6教程】

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]
複製程式碼
1.3 複用Symbol

Symbol雖然旨在給我們提供一個永遠不會重複的特殊值,但是也確實會存在需要相同Symbol的場景,API永遠比我們想得多,Symbol有一個方法:Symbol.for,它接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字串為名稱的 Symbol 值。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true
複製程式碼

這個API的工作原理大概流程是這樣的:Symbol.for()與Symbol()這兩種寫法,都會生成新的 Symbol。它們的區別是,前者會被登記在全域性環境中供搜尋,後者不會。Symbol.for()不會每次呼叫就返回一個新的 Symbol 型別的值,而是會先檢查給定的key是否已經存在,如果不存在才會新建一個值。比如,如果你呼叫Symbol.for("cat")30 次,每次都會返回同一個 Symbol 值,但是呼叫Symbol("cat")30 次,會返回 30 個不同的 Symbol 值。【參考自阮一峰的ES6教程】 同樣的,Symbol.keyFor這個API可以返回一個已登記的 Symbol 型別值的key。

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
//因為s2不是以Symbol.for()建立的,所以使用Symbol.keyFor()無法得到其值
複製程式碼

2. 通用方法

當然,使用的時候為了更容易區分,一般而言我們會向Symbol()裡面傳遞一個引數為了區分不同的Symbol,例如Symbol('name')這樣,這樣更有利於開發時進行除錯

let symbol1 = Symbol('name');
let symbol12 = Symbol('age');

symbol1 // Symbol(name)
symbol2 // Symbol(age)
//上方的只是返回值而已,即使返回值相同,也不代表這兩個Symbol相同!

symbol1.toString() // "Symbol(name)"
symbol2.toString() // "Symbol(age)"

//另外一個就是Symbol 值不能與其他型別的值進行運算,會報錯。
Symbol('hero') + ' hello'  //TypeError: can't convert symbol to string
//如果真的要處理,我們只能先顯式的將Symbol轉化為字串才能和其他數字或字串相加
Symbol('hero').toString() + ' hello'     //"Symbol(hero) hello"
//Symbol可以轉化為布林值
Boolean(Symbol('hero'))     //true
//Symbol無法通過某種方式直接轉化為數字
Number(Symbol('hero')) // TypeError
複製程式碼

三、結語

Symbol的出現目的很單一,就是作為物件的鍵值,常用的一些方法也非常好理解,關鍵是這個可以非常有效的消除magic string和magic number啊喂?。

相關文章