> 原文出處:http://www.infoq.com/cn/articles/es6-in-depth-symbols
你是否知道ES6中的Symbols是什么,它有什么作用呢?我相信你很可能不知道,那就讓我們一探究竟!
[TOC]
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的代碼。
1. 你創建的屬性很可能影響到其它使用了`for-in`或`Object.keys()`的代碼。
2. 一些聰明的庫作者可能已經考慮并使用了這項技術,這樣一來你的庫就會與已有的庫產生某些沖突
3. 當然,很可能你比他們更聰明,你先采用了這項技術,但是他們的庫仍然無法與你的庫默契配合。
4. 標準委員會可能決定為所有的元素增加一個.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](https://en.wikipedia.org/wiki/Symbol_%28programming%29)類似,但不像它們集成得那么緊密。在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循環](http://www.infoq.com/cn/articles/es6-in-depth-iterators-and-the-for-of-loop)》中,我們已經領略了借助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](https://github.com/zloirock/core-js#ecmascript-6-symbols)。因為symbol與其它類型不盡相同,所以polyfill目前不是很完美。[請閱讀注意事項](https://github.com/zloirock/core-js#caveats-when-using-symbol-polyfill)。
下一篇文章,我們將奉上一篇Gastón Silva的文章,講解如何使用Babel和Broccoli來接觸更多的ES6新特性,借鑒這篇文章的經驗你可以輕松地開始ES6之旅。
接 下來,我們將深入淺出Collections,這個特性被期待已久,最終在ES6版本加入到JavaScript中。我們將回溯到編程的起源,探索兩個古 老的特性,緊接著討論兩個非常相似的特性,它們的生命周期短,但是威力巨大。所以請記得回來,一起探索接下來的旅程!到時見!