Symbol 是什麼?
Symbols 不是圖示,也不是指在程式碼中可以使用小圖片:
也不是指代其他一些東西的語法。那麼,Symbol 到究竟是什麼呢?
七種資料型別
JavaScript 在 1997 年被標準化時,就有 6 種資料型別,直到 ES6 出現之前,程式中的變數一定是以下 6 種資料型別之一:
- Undefined
- Null
- Boolean
- Number
- String
- Object
每種資料型別都是一系列值的組合,前面 5 種資料型別值的數量都是有限的。Boolean
型別只有兩個值:true
和 false
,為 Boolean
型別的變數賦值時,並不會產生新的值(共享了true
和 false
這兩個值)。對於 Number
和 String
來說,它們的值則多得多了,標準的說法是有 18,437,736,874,454,810,627 個 Number
型別的值(包括 NAN
)。String
型別的個數就難以統計了,我原以為是 (2144,115,188,075,855,872 − 1) ÷ 65,535…不過也許我算錯了。
物件值的個數是無限的,每個物件都是獨一無二的,每次開啟一個網頁,都建立了一系列的物件。
ES6 中的 Symbol 也是一種資料型別,但是不是字串,也不是物件,而是一種新的資料型別:第七種資料型別。
下面我們來看一個場景,也許 Symbol 能派上用場。
一個布林值引出的問題
有時,把一些屬於其他物件的資料暫存在另一個物件中是非常方便的。例如,假設你正在編寫一個 JS 庫,使用 CSS 中的 transition 來讓一個 DOM 元素在螢幕上飛奔,你已經知道不能同時將多個 transition 應用在同一個 div
上,否則將使得動畫非常不美觀,你也確實有辦法來解決這個問題,但是首先你需要知道該 div
是否已經在移動中。
怎麼解決這個問題呢?
其中一個方法是使用瀏覽器提供的 API 來探測元素是否處於動畫狀態,但殺雞焉用牛刀,在將元素設定為移動時,你的庫就知道了該元素正在移動。
你真正需要的是一種機制來跟蹤哪些元素正在移動,你可以將正在移動的元素儲存在一個陣列中,每次要為一個元素設定動畫時,首先檢查一下這個元素是否已經在這個列表中。
啊哈,但是如果你的陣列非常龐大,即便是這樣的線性搜尋也會產生效能問題。
那麼,你真正想做的就是直接在元素上設定一個標誌:
1 2 3 4 |
if (element.isMoving) { smoothAnimations(element); } element.isMoving = true; |
這也有一些潛在的問題,不得不承認這樣一個事實:還有其他程式碼也可能操作該 ODM 元素。
- 在其他程式碼中,你建立的屬性會被
for-in
或Object.keys()
列舉出來; - 在其他一些庫中也許已經使用了同樣的方式(在元素上設定了相同的屬性),那麼這將和你的程式碼發生衝突,產生不可預計的結果;
- 其他一些庫可能在將來會使用同樣的方式,這也會與你的程式碼發生衝突;
- 標準委員會可能會為每個元素新增一個
.isMoving()
原生方法,那麼你的程式碼就徹底不能工作了。
當然,對於最後三個問題,你可以選擇一個無意義的不會有人會使用到的字串:
1 2 3 4 |
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; |
這似乎太不靠譜了,看了讓人眼睛痛。
你還可以用加密演算法來生成一個幾乎唯一的字串:
1 2 3 4 5 6 7 8 9 |
// get 1024 Unicode characters of gibberish var isMoving = SecureRandom.generateName(); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true; |
object[name]
語法允許我們將任何字串作為屬性名,程式碼能正常工作,衝突幾乎是不可能了,程式碼看起來也美觀多了。
但是,這回導致糟糕的除錯體驗,每次使用 console.log()
列印出包含該屬性的元素時,你回看到一個龐大的垃圾字串,並且如果還不止一個這樣的屬性呢?每次重新整理後屬性名都發生了變化,怎麼樣使這些屬性看起來更加直觀呢?
為什麼這麼難?我們只是為了儲存一個小小的標誌位。
用 Symbol 來解決問題
Symbol 值可以由程式建立,並可以作為屬性名,而且不用擔心屬性名衝突。
1 |
var mySymbol = Symbol(); |
呼叫 Symbol()
方法將建立一個新的 Symbol 型別的值,並且該值不與其它任何值相等。
與數字和字串一樣,Symbol 型別的值也可以作為物件的屬性名,正是由於它不與任何其它值相等,對應的屬性也不會發生衝突:
1 2 |
obj[mySymbol] = "ok!"; // guaranteed not to collide console.log(obj[mySymbol]); // ok! |
下面是使用 Symbol 來解決上面的問題:
1 2 3 4 5 6 7 8 9 |
// create a unique symbol var isMoving = Symbol("isMoving"); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true; |
上面程式碼需要注意幾點:
- 方法
Symbol("isMoving")
中的"isMoving"
字串被稱為 Symbol 的描述資訊,這對除錯非常有幫助。可以通過console.log(isMoving)
列印出來,或通過isMoving.toString()
將isMoving
轉換為字串時,或在一些錯誤資訊中顯示出來。 element[isMoving]
訪問的是 symbol-keyed 屬性,除了屬性名是 Symbol 型別的值之外,與其它屬性都一樣。- 和陣列一樣,symbol-keyed 屬性不能通過
.
操作符來訪問,必須使用方括號的方式。 - 操作 symbol-keyed 屬性也非常方便,通過上面程式碼我們已經知道如何獲取和設定
element[isMoving]
的值,我們還可以這樣使用:if (isMoving in element)
或delete element[isMoving]
。 - 另一方面,只有在
isMoving
的作用域範圍內才可以使用上述程式碼,這可以實現弱封裝機制:在一個模組內建立一些 Symbol,只有在該模組內部的物件才能使用,而不用擔心與其它模組的程式碼發生衝突。
由於 Symbol 的設計初衷是為了避免衝突,當遍歷 JavaScript 物件時,並不會列舉到以 Symbol 作為建的屬性,比如,for-in
迴圈只會遍歷到以字串作為鍵的屬性,Object.keys(obj)
和 Object.getOwnPropertyNames(obj)
也一樣,但這並不意味著 Symbol 為鍵的屬性是不可列舉的:使用 Object.getOwnPropertySymbols(obj)
這個新方法可以列舉出來,還有 Reflect.ownKeys(obj)
這個新方法可以返回物件中所有字串和 Symbol 鍵。(我將在後面的文章中詳細介紹 Reflect
這個新特性。)
庫和框架的設計者將會發現很多 Symbol 的用途,稍後我們將看到,JavaScript 語言本身也對其有廣泛的應用。
Symbol 究竟是什麼呢
1 2 |
> typeof Symbol() "symbol" |
Symbol 是完全不一樣的東西。一旦建立後就不可更改,不能對它們設定屬性(如果在嚴格模式下嘗試這樣做,你將得到一個 TypeError)。它們可以作為屬性名,這時它們和字串的屬性名沒有什麼區別。
另一方面,每個 Symbol 都是獨一無二的,不與其它 Symbol 重複(即便是使用相同的 Symbol 描述建立),建立一個 Symbol 就跟建立一個物件一樣方便。
ES6 中的 Symbol 與傳統語言(如 Lisp 和 Ruby)中的 Symbol 中的類似,但並不是完全照搬到 JavaScript 中。在 Lisp 中,所有識別符號都是 Symbol;在 JavaScript 中,識別符號和大多數屬性仍然是字串,Symbol 只是提供了一個額外的選擇。
值得注意的是:與其它型別不同的是,Symbol 不能自動被轉換為字串,當嘗試將一個 Symbol 強制轉換為字串時,將返回一個 TypeError。
1 2 3 4 5 |
> 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.for(string) 從 Symbol 登錄檔中返回相應的 Symbol,與上個方法不同的是,Symbol 登錄檔中的 Symbol 是共享的。也就是說,如果你呼叫
Symbol.for("cat")
三次,都將返回相同的 Symbol。當不同頁面或同一頁面不同模組需要共享 Symbol 時,登錄檔就非常有用。 - Symbol.iterator 返回語言預定義的一些 Symbol,每個都有其特殊的用途。
如果你仍不確定 Symbol 是否有用,那麼接下來的內容將非常有趣,因為我將為你演示 Symbol 的實際應用。
Symbol 在 ES6 規範中的應用
我們已經知道可以使用 Symbol 來避免程式碼衝突。之前在介紹 iterator 時,我們還解析了 for (var item of myArray)
內部是以呼叫 myArray[Symbol.iterator]()
開始的,當時我提到這個方法可以使用 myArray.iterator()
來代替,但是使用 Symbol 的後向相容性更好。
在 ES6 中還有一些地方使用到了 Symbol。(這些特性還沒有在 FireFox 中實現。)
- 使
instanceof
可擴充套件。在 ES6 中,object instanceof constructor
表示式被標準化為建構函式的一個方法:constructor[Symbol.hasInstance](object)
,這意味著它是可擴充套件的。 - 消除新特性和舊程式碼之間的衝突。
- 支援新型別的字串匹配。在 ES5 中,呼叫
str.match(myObject)
時,首先會嘗試將myObject
轉換為RegExp
物件。在 ES6 中,首先將檢查myObject
中是否有myObject[Symbol.match](str)
方法,在所有正規表示式工作的地方都可以提供一個自定義的字串解析方法。
這些用途還比較窄,但僅僅通過我文章中的程式碼很難看到這些新特性產生的重大影響。JavaScript 的 Symbol 是 PHP 和 Python 中 __doubleUnderscores
的改進版本,標準組織將使用它來為語言新增新特性,而不會對已有程式碼產生影響。
相容性
Firefox 36 和 Chrome 38 實現了 Symbol,並且 Firefox 的實現者是本文的原文作者,所以有什麼問題可以直接聯絡作者。
對於還沒有原生支援 Symbol 的瀏覽器,你可以使用 polyfill,如 core.js,但該 polyfill 實現並不完美,請閱讀注意事項。