你是否知道ES6中的Symbols是什麼,它有什麼作用呢?我相信你很可能不知道,那就讓我們一探究竟!
Symbols並非用來指代某種Logo。
它們也不是可以用作代碼的小圖標。
它們不是代替其它東西的文學手法。
它們更不可能被用來指代諧音詞Cymbals(鐃鈸)。
(編程的時候最好不要演奏鐃鈸,它們太過吵鬧,很可能導致你的程序崩潰。)
那麼,Symbols到底是什麼呢?
它是JavaScript的第七種原始類型
1997年JavaScript首次被標準化,那時只有六種原始類型,在ES6以前,JS程序中使用的每一個值都是以下幾種類型之一:
- Undefined 未定義
- Null 空值
- Boolean 布爾類型
- Number 數字類型
- String 字符串類型
- Object 對象類型
每種類型都是多個值的集合,前五個集合是有限的。布爾類型只有兩個值,true
和false
,不會再創造第三種布爾值;數字類型和字符串類型的值更多,標準指明一共有18,437,736,874,454,810,627種不同的數字(包括NaN
, 亦即“Not a Number”的縮寫,代表非數字),可能存在的字符串類型的值擁有無以匹敵的數量,我估算了一下大約是 (2144,115,188,075,855,872 − 1) ÷ 65,535種……當然,我很可能得出了一個錯誤的答案,但字符串類型值的集合一定是有限的。
然而,對象類型值的集合是無限的。每一個對象都像珍貴的雪花一樣獨一無二,每一次你打開一個Web頁面,都會創建一堆對象。
ES6新特性中的symbol也是值,但它不是字符串,也不是對象,而是是全新的——第七種類型的原始值。
讓我們一起探討一下symbol的實際應用場景。
從一個簡單的布爾類型出發
有時候你可以非常輕鬆地將別人的外部數據存儲到一個JavaScript對象中。
舉 個例子,假設你正在寫一個JS庫,可以通過CSS transitions使DOM元素在屏幕上移動。你可能會注意到,當你嘗試在一個div元素上同時應用多重CSS transitions時並不會生效。實際效果是醜陋而又不連續的“跳閃”。你認爲可以修復這個問題,但前提是你需要一種發現給定元素是否已經移動過的方 法。
應當如何解決這個問題呢?
一種方法是,用CSS API來告訴瀏覽器元素是否正在移動,但這樣簡直小題大做。在元素移動的第一時間內你的庫就應該記錄下移動的狀態,所以它自然知道元素正在移動。
你真正想要的是一種持續跟蹤某個元素正在移動的方法。你可以維護一個數組,記錄所有正在移動的元素,每當你的庫被調用來移動某個元素時,你可以檢索數組來查看元素是否已經存在,亦即它是否正在移動中。
當然,如果數組非常大的話,線性搜索將會非常緩慢。
實際上你只想爲元素設置一個標記:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
這樣也會有一些潛在的問題,事實上,你的代碼很可能不是唯一一段操作DOM的代碼。
- 你創建的屬性很可能影響到其它使用了
for-in
或Object.keys()
的代碼。 - 一些聰明的庫作者可能已經考慮並使用了這項技術,這樣一來你的庫就會與已有的庫產生某些衝突
- 當然,很可能你比他們更聰明,你先採用了這項技術,但是他們的庫仍然無法與你的庫默契配合。
- 標準委員會可能決定爲所有的元素增加一個.isMoving()方法,到那時你需要重寫相關邏輯,必定會有深深的挫敗感。
當然你可以選擇一個乏味而愚蠢的命名(其他人根本不會想用的那些名稱)來解決最後的三個問題:
if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
這隻會造成無畏的眼疲勞。
藉助於密碼學,你可以生成一個唯一的屬性名稱:
// 獲取1024個Unicode字符的無意義命名
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
object[name]
語法允許你使用幾乎任何字符串作爲屬性名稱。所以這個方法行之有效:衝突幾乎是不可能的,並且你的代碼看起來也很簡潔。
但是這也將帶來不良的調試體驗。每當你在控制檯輸出(console.log()
)包含那個屬性的元素時,你將會看到一堆巨大的字符串垃圾。假使你需要比這多得多的類似屬性呢?你如何保持它們整齊劃一?每當你重載的時候它們的命名甚至都不一樣!
爲什麼這個問題如此困難?我們只想要一個小小的布爾值啊!
symbol是最終的解決方案
symbol是程序創建並且可以用作屬性鍵的值,並且它能避免命名衝突的風險。
var mySymbol = Symbol();
調用Symbol()
創建一個新的symbol,它的值與其它任何值皆不相等。
字符串或數字可以作爲屬性的鍵,symbol也可以,它不等同於任何字符串,因而這個以symbol爲鍵的屬性可以保證不與任何其它屬性產生衝突。
obj[mySymbol] = "ok!"; // 保證不會衝突
console.log(obj[mySymbol]); // ok!
想要在上述討論的場景中使用symbol,你可以這樣做:
// 創建一個獨一無二的symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
有關這段代碼的一些解釋:
-
Symbol("isMoving")
中的isMoving
被稱作描述。你可以通過console.log()
將它打印出來,對調試非常有幫助;你也可以用.toString()
方法將它轉換爲字符串呈現;它也可以被用在錯誤信息中。 -
element[isMoving]
被稱作一個以symbol爲鍵(symbol-keyed)的屬性。簡而言之,它的名字是symbol
而不是一個字符串。除此之外,它與一個普通的屬性沒有什麼區別。 -
以symbol爲鍵的屬性屬性與數組元素類似,不能被類似
obj.name
的點號法訪問,你必須使用方括號訪問這些屬性。 -
如果你已經得到了symbol,那麼訪問一個以symbol爲鍵的屬性同樣簡單,以上的示例很好地展示瞭如何獲取
element[isMoving]
的值以及如何爲它賦值。如果我們需要,可以查看屬性是否存在:if (isMoving in element)
,也可以刪除屬性:delete element[isMoving]
。 -
另一方面,只有當
isMoving
在當前作用域中時纔會生效。這是symbol的弱封裝機制:模塊創建了幾個symbol,可以在任意對象上使用,無須擔心與其它代碼創建的屬性產生衝突。
symbol鍵的設計初衷是避免初衷,因此JavaScript中最常見的對象檢查的特性會忽略symbol鍵。例如,for-in
循環只會遍歷對象的字符串鍵,symbol鍵直接跳過,Object.keys(obj)
和Object.getOwnPropertyNames(obj)
也是一樣。但是symbols也不完全是私有的:用新的API
Object.getOwnPropertySymbols(obj)
就可以列出對象的symbol鍵。另一個新的API,Reflect.ownKeys(obj)
,會同時返回字符串鍵和symbol鍵。(我們將在隨後的文章中講解Reflect(反射) API)。
慢慢地我們會發現,越來越多的庫和框架將大量使用symbol,語言本身也會將symbol應用於廣泛的用途。
但是,到底什麼是symbol呢?
> typeof Symbol()
"symbol"
確切地說,symbol與其它類型並不完全相像。
symbol被創建後就不可變更,你不能爲它設置屬性(在嚴格模式下嘗試設置屬性會得到TypeError的錯誤)。他們可以用作屬性名稱,這些性質與字符串類似。
另一方面,每一個symbol都獨一無二,不與其它symbol等同,即使二者有相同的描述也不相等;你可以輕鬆地創建一個新的symbol。這些性質與對象類似。
ES6中的symbol與Lisp和Ruby這些語言中更傳統的symbol類似,但不像它們集成得那麼緊密。在Lisp中,所有的標識符都是symbol;在JS中,標識符和大多數的屬性鍵仍然是字符串,symbol只是一個額外的選項。
關於symbol的忠告:symbol不能被自動轉換爲字符串,這和語言中的其它類型不同。嘗試拼接symbol與字符串將得到TypeError錯誤。
> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: can't convert symbol to string
> `your symbol is ${sym}`
// TypeError: can't convert symbol to string
通過String(sym)
或sym.toString()
可以顯示地將symbol轉換爲一個字符串,從而回避這個問題。
獲取symbol的三種方法
有三種獲取symbol的方法。
-
調用Symbol()。正如我們上文中所討論的,這種方式每次調用都會返回一個新的唯一symbol。
-
調用Symbol.for(string)。這種方式會訪問symbol註冊表,其中存儲了已經存在的一系列symbol。這種方式與通過
Symbol()
定義的獨立symbol不同,symbol註冊表中的symbol是共享的。如果你連續三十次調用Symbol.for("cat")
,每次都會返回相同的symbol。註冊表非常有用,在多個web頁面或同一個web頁面的多個模塊中經常需要共享一個symbol。 -
使用標準定義的symbol,例如:Symbol.iterator。標準根據一些特殊用途定義了少許的幾個symbol。
如果你尚不確定symbol是否實用,最後這一章將向你展示symbol在實際應用中發揮的巨大作用,非常有趣!
symbol在ES6規範中的應用
在之前的文章《深入淺出ES6(二):迭代器和for-of循環》中,我們已經領略了藉助ES6 symbol的力量避免代碼衝突的方法,循環for (var item of myArray)
首先調用myArray[Symbol.iterator]()
,當時我提到這種寫法是爲了替代myArray.iterator()
,擁有更好的向後兼容性。
現在我們知道symbol到底是什麼了,自然很容易理解爲什麼我們要創造一個symbol以及它爲我們帶來什麼新特性。
ES6中還有其它幾處使用了symbol的地方。(這些特性在Firefox裏尚未實現。)
-
使instanceof可擴展。在ES6中,表達式
object instanceof constructor
被指定爲構造函數的一個方法:constructor[Symbol.hasInstance](object)
。這意味着它是可擴展的。 -
消除新特性和舊代碼之間的衝突。這一點非常複雜,但是我們發現,添加某些ES6數組方法會破壞現有的Web網站。其它Web標準有相同的問題:向瀏覽器中添加新方法會破壞原有的網站。然而,破壞問題主要由動態作用域引起,所以ES6引入一個特殊的symbol——
Symbol.unscopables
,Web標準可以用這個symbol來阻止某些方法別加入到動態作用域中。 -
支持新的字符串匹配類型。在ES5中,
str.match(myObject)
會嘗試將myObject
轉換爲正則表達式對象(RegExp
)。在ES6中,它會首先檢查myObject
是否有一個myObject[Symbol.match](str)
方法。現在的庫可以提供自定義的字符串解析類,所有支持RegExp
對象的環境都可以正常運行。
這些用例的應用範圍都非常小,很難看到這些特性通過它們自身影響我們每日的代碼,長期來看才能體現它們的價值。實際上,symbol是PHP和Python中的__doubleUnderscores
在JavaScript語言環境中的改進版。標準將藉助symbol的力量在未來向語言中添加新的鉤子,同時無風險地將新特性添加到你已有的代碼中。
我何時可以使用ES6 symbol?
symbol在Firefox 36和Chrome 38中均已被實現。Firefox中的實現由我親自完成,所以如果你的symbol像鐃鈸(cymbals)一樣行爲異常,請直接聯繫我!
爲了支持那些尚未支持原生ES6 symbol的瀏覽器,你可以使用一個polyfill,例如core.js。因爲symbol與其它類型不盡相同,所以polyfill目前不是很完美。請閱讀注意事項。