深入淺出ES6(八):Symbols

你是否知道ES6中的Symbols是什麼,它有什麼作用呢?我相信你很可能不知道,那就讓我們一探究竟!

Symbols並非用來指代某種Logo。

它們也不是可以用作代碼的小圖標。

它們不是代替其它東西的文學手法。

它們更不可能被用來指代諧音詞Cymbals(鐃鈸)。

(編程的時候最好不要演奏鐃鈸,它們太過吵鬧,很可能導致你的程序崩潰。)

那麼,Symbols到底是什麼呢?

它是JavaScript的第七種原始類型

1997年JavaScript首次被標準化,那時只有六種原始類型,在ES6以前,JS程序中使用的每一個值都是以下幾種類型之一:

  • Undefined 未定義
  • Null 空值
  • Boolean 布爾類型
  • Number 數字類型
  • String 字符串類型
  • Object 對象類型

每種類型都是多個值的集合,前五個集合是有限的。布爾類型只有兩個值,truefalse,不會再創造第三種布爾值;數字類型和字符串類型的值更多,標準指明一共有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-inObject.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類似,但不像它們集成得那麼緊密。在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目前不是很完美。請閱讀注意事項

發佈了99 篇原創文章 · 獲贊 56 · 訪問量 84萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章