ES6 Symbol 的用途
JavaScript/Reference/Global_Objects/Symbol" rel="nofollow,noindex" target="_blank">Symbol 唯一的用途就是標識物件屬性,表明物件支援的功能。 相比於字元屬性名,Symbol 的區別在於唯一,可避免名字衝突。 這樣 Symbol 就給出了唯一標識型別資訊的一種方式,從這個角度看有點類似 C++ 的Traits。
解決了什麼問題
在 JavaScript 中要判斷一個物件支援的功能,常常需要做一些 Duck Test。 比如經常需要判斷一個物件是否可以按照陣列的方式去迭代,這類物件稱為 Array-like。lodash 中是這樣判斷的:
function isArrayLike(value) { return value != null && isLength(value.length) && !isFunction(value); }
在 ES6 中提出一個@@iterator
方法,所有支援迭代的物件(比如Array
、Map
、Set
)都要實現。@@iterator
方法的屬性鍵為Symbol.iterator
而非字串。
這樣只要物件定義有Symbol.iterator
屬性就可以用for ... of
進行迭代。
比如:
if (Symbol.iterator in arr) { for(let n of arr) console.log(n) }
其他用例
上述例子中 Symbol 標識了這個物件是可迭代的(Iterables),是一個典型的 Symbol 用例。 詳情可以參考ES6 迭代器 一文。 此外利用 Symbol 還可以做很多其他事情,例如:
常量列舉
JavaScript 沒有列舉型別,常量概念也通常用字串或數字表示。例如:
const COLOR_GREEN = 1 const COLOR_RED = 2 function isSafe(trafficLight) { if (trafficLight === COLOR_RED) return false if (trafficLight === COLOR_GREEN) return true throw new Error(`invalid trafficLight: ${trafficLight}`) }
-
我們需要認真地排列這些常量的值。如果不小心有兩個值重複會很難除錯,就像
#define false true
引起的問題一樣。 -
取值可能重複。如果有另一處定義了
BUSY = 1
並不小心把BUSY
傳入,乾脆isSafe(1)
,理想的列舉概念應該丟擲異常,但上述程式碼無法檢測。
Symbol 給出瞭解決方案:
const COLOR_GREEN = Symbol('green') const COLOR_RED = Symbol('red')
即使字串寫錯或重複也不重要,因為每次呼叫Symbol()
都會給出獨一無二的值。
這樣就可以確保所有isSafe()
呼叫都傳入這兩個 Symbol 之一。
私有屬性
由於沒有訪問限制,JavaScript 曾經有一個慣例:私有屬性以下劃線起始來命名。 這樣不僅無法隱藏這些名字,而且會搞壞程式碼風格。 可以利用 Symbol 來隱藏這些私有屬性:
let speak = Symbol('speak') class Person { [speak]() { console.log('harttle') } }
如下幾種訪問都獲取不到speak
屬性:
let p = new Person() Object.keys(p)// [] Object.getOwnPropertyNames(p)// [] for(let key in p) console.log(key)// <empty>
但 Symbol 只能隱藏這些函式,並不能阻止未授權訪問。
仍然可以通過Object.getOwnPerpertySymbols()
,Reflect.ownKeys(p)
來列舉到speak
屬性。
新的基本型別
Symbol 是新的基本型別,從此JavaScript 有 7 種類型 :
Number Boolean String undefined null Symbol Object
轉換為字串
Symbol 支援symbol.toString()
方法以及String(symbol)
,
但不能通過+
轉換為字串,也不能直接用於模板字串輸出。
後兩種情況都會產生TypeError
,是為了避免把它當做字串屬性名來使用。
轉換為數字
不可轉換為數字。Number(symbol)
或四則運算都會產生TypeError
。
轉換為布林
Boolean(symbol)
和取非運算都 OK。這是為了方便判斷是否包含屬性。
包裹物件
Symbol 是基本型別,但不能用new Symbol(sym)
來包裹成物件,需要使用Object(sym)
。
除了判等不成立外,包裹物件的使用與原基本型別幾乎相同:
let sym = Symbol('author') let obj = { [sym]: 'harttle' } let wrapped = Object(sym) wrapped instanceof Symbol// true,真的是true!!! obj[sym]// 'harttle' obj[wrapped]// 'harttle'
常見的 Symbol
文章最前面的例子提到的Symbol.iterator
是一個內建 Symbol。除此之外常見的內建 Symbol 還有:
Symbol.match
Symbol.match
在String.prototype.match()
中用於獲取RegExp
物件的匹配方法。
我們來改寫一下Symbol.match
標識的方法,觀察String.prototype.match()
的表現,
下面的例子來自 MDN:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match class RegExp1 extends RegExp { [Symbol.match](str) { var result = RegExp.prototype[Symbol.match].call(this, str); return result ? 'VALID' : 'INVALID'; } } console.log('2012-07-02'.match(new RegExp1('([0-9]+)-([0-9]+)-([0-9]+)'))); // expected output: "VALID"
Symbol.toPrimitive
在物件進行運算時經常會變成"[object Object]"
,
這是物件轉換為字串(基本資料型別)的預設行為,定義在Object.prototype.toString
。
比如這個物件:
var count = { value: 3 }; count + 2// "[object Object]2"
這個物件也在表示一個數字,怎麼讓它可以參加四則運算呢?
給它加一個Symbol.toPrimitive
屬性,來改變它轉換為基本型別的行為:
count[Symbol.toPrimitive] = function () { return this.value }; count + 2// 5
更多內建 Symbol 請參考 MDN 文件:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#Well-known_symbols
跨 Realm 使用
JavaScript Realm 是指當前程式碼片段執行的上下文,包括全域性變數,比如Array
,Date
這些全域性函式。
在開啟新標籤頁、 載入 iframe 或載入 Worker 程序時,都會產生多個 JavaScript Realm。
跨 Realm 通訊時這些全域性變數是不同的,例如從 iframe 中傳遞給陣列arr
給父視窗,
父視窗中收到的arr instanceof Array
為false
,因為它的原型是 iframe 中的那個Array
。
但是一個物件在 iframe 中可以迭代(Iterable),那麼在父視窗中也應當能被迭代。
這就要求 Symbol 可以跨 Realm,當然Symbol.iterator
可以。
如果你定義的 Symbol 也需要跨 Realm,請使用 Symbol Registry API:
// 在 Symbol Registry 中註冊一個跨 Realm Symbol let sym = Symbol.for('foo') // 獲取 Symbol 的鍵值字串 Symbol.keyFor(sym)// 'foo'
內建的跨 Realm Symbol 其實不在 Symbol Registry 中:
Symbol.keyFor(Symbol.iterator)// undefined