[ES6深度解析]7:符號(Symbols)


第七種類型

自從JavaScript在1997年首次標准化以來,已經有了六種類型。在ES6之前,JS程序中的每個值都屬於這些類別之一:

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object
    每種類型都是一組值。前五個集合都是有限的。當然,只有兩個布爾值,truefalse,而且它們不會產生新的值。有更多的Number和String值。該標准稱,共有18,437,736,874,454,810,627個不同的數字(包括NaN,即非數字的縮寫)。與可能的字符串的數量相比,這簡直是九牛一毛。

然而,Object值的集合是開放式的。每一件物品都是獨一無二的、珍貴的雪花。每次打開Web頁面時,都會創建大量新對象。

ES6 Symbols是值,但不是字符串。他們不是對象。它們是新的東西:第七種類型的值。讓我們來談談它們可能會派上用場的情況。

一個簡單的布爾值

有時,將一些額外的數據存儲在真正屬於其他人的JavaScript對象上是非常方便的。例如,假設您正在編寫一個JS庫,它使用CSS轉換使DOM元素在屏幕上快速移動。你已經注意到,嘗試在單個div上同時應用多個CSS過渡是行不通的。它會導致丑陋的、不連續的“跳躍”。你認為可以修復這個問題,但首先你需要一種方法來確定給定元素是否已經在移動。

這個問題該如何解決?

一種方法是使用CSS APIs詢問瀏覽器元素是否在移動。但這聽起來有點過分了。你的庫應該已經知道元素在移動;這是一開始讓它移動的代碼!你真正需要的是一種跟蹤哪些元素在移動的方法。你可以保存一個包含所有移動元素的數組。每次調用庫動畫元素時,都可以搜索數組,查看該元素是否已經存在。但是如果數組很大,線性搜索會很慢。

另一個辦法是在元素上設置一個標志:

if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

這也存在一些潛在的問題。它們都與這樣一個事實有關:你的代碼並不是唯一使用DOM的代碼。

  • 其他使用for-inObject.keys()的代碼可能會在你創建的屬性上出錯。
  • 其他一些聰明的庫作者可能首先想到了這種技術,因此你的庫與現有庫的交互會很糟糕。(屬性名重復了,會有沖突)
  • 其他一些聰明的庫作者可能會在未來想到它,而你的js庫與那個未來的庫進行糟糕的交互。
  • 標准委員會可能決定向所有元素添加.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;

這代碼辣眼睛!

你還可以使用加密技術為屬性生成一個實際唯一的名稱:

// get 1024 Unicode characters of gibberish
var isMoving = SecureRandom.generateName();

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

Object[name]語法允許使用任意字符串作為屬性名。所以這是可行的:命名沖突實際上是不可能的,你的代碼看起來是正常的。但這將導致糟糕的調試體驗。每當你在console.log()中添加一個帶有該屬性的元素時,將看到一個巨大的垃圾字符串。如果你需要不止一個這樣的屬性呢?你是如何讓它們保持一致的?每次重新加載時,它們都會有不同的名稱。

為什么這么難?我們只需要一個布爾值!

Symbols就是你要的答案

Symbols是程序可以創建並用作屬性鍵的值,而不會有名稱沖突的風險

var mySymbol = Symbol();

調用Symbol()將創建一個新的符號,該符號的值不等於任何其他值。

就像字符串或數字一樣,可以使用符號作為屬性鍵。因為它不等於任何字符串,所以這個符號鍵控屬性保證不會與任何其他屬性發生沖突。

obj[mySymbol] = "ok!";  // mySymbol是不會重復的屬性名
console.log(obj[mySymbol]);  // ok!

以下是在上面討論的情況下如何使用符號:

// create a unique symbol
var isMoving = Symbol("isMoving");

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

關於這段代碼的幾點注意事項:

  • Symbol("isMoving")中的字符串"isMoving"被稱為描述。這對調試很有幫助。當你將symbol寫入console.log()時,當使用.tostring()將其轉換為字符串時,以及可能在錯誤消息中都會顯示它。這就是描述的用途。
  • element[isMoving]被稱為符號鍵控屬性(symbol-keyed property)。它只是一個名稱是符號而不是字符串的屬性。除此之外,它在任何方面都是正常的性質。
  • 與數組元素一樣,符號鍵控屬性不能使用點語法訪問,如obj.name。必須使用方括號訪問它們。
  • 如果已經獲得了符號鍵控屬性,那么訪問該符號鍵控屬性是很簡單的。上面的例子展示了如何獲取和設置element[isMoving],我們還可以詢問if (isMoving in element),甚至如果需要的話可以刪除delete element[isMoving]
  • 另一方面,只要isMoving在作用域內,所有這些都是可能的。這使得Symbol成為一種弱封裝機制:為自己創建一些Symbol的模塊可以在任何它想要的對象上使用它們,而不必擔心與其他代碼創建的屬性沖突

因為符號鍵(symbol keys)是為了避免沖突而設計的,所以JavaScript最常見的對象檢查特性就是簡單地忽略符號鍵。例如,for-in循環只在對象的字符串鍵上循環。跳過符號鍵。Object.keys(obj)Object.getOwnPropertyNames(obj)做同樣的事情。但是Symbol並不是完全私有的:可以使用新的APIObject. getownpropertysymbols(obj)來列出對象的符號鍵。另一個新的APIReflect.ownKeys(obj)同時返回字符串和符號鍵。

但到底什么是符號Symbols呢?

> typeof Symbol()
"symbol"

Symbols和其他東西不完全一樣。

它們一旦被創造就不可改變。你不能在它們上設置屬性(如果你在嚴格模式下嘗試,你會得到一個TypeError)。它們可以是屬性名。這些都是類似String的性質。

另一方面,每個Symbol都是獨一無二的,不同於所有其他符號(甚至其他具有相同描述的符號),你可以輕松創建新的符號。這些都是類似Object的特性。

ES6 Symbol類似於Lisp和Ruby等語言中更傳統的符號,但並沒有緊密地集成到語言中。在Lisp中,所有標識符都是Symbols。在JS中,標識符和大多數屬性鍵仍然被認為是字符串。Symbols只是一個額外的選擇。

關於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.for(string)。這將訪問一組稱為符號注冊表(symbol registry)的現有符號(symbol)。與Symbol()定義的唯一符號不同,符號注冊表中的符號是共享的。如果你調用Symbol.for("cat") 30次,它每次都會返回相同的符號。當多個網頁或同一網頁中的多個模塊需要共享一個符號時,注冊表是有用的。
  • 使用由標准定義的符號,比如Symbol.iterator。一些符號是由標准本身定義的。每一個都有它自己的特殊目的。

Symbol的應用

Symbol.iterator

我們已經看到了ES6使用符號來避免與現有代碼沖突的一種方法。在關於迭代器的文章中,我們看到for (var item of myArray)的循環首先調用myArray[Symbol.iterator]()。這個方法可以被稱為myArray.iterator(),但是symbol符號更利於代碼向后兼容。

讓instanceof可擴展

在ES6中,表達式 object instanceof constructor被指定為構造函數constructor的一個方法:constructor[Symbol.hasInstance](object)。這意味着它是可擴展的。

消除新特性和舊代碼之間的沖突

某些ES6 Array方法僅僅出現在代碼里就破壞了現有的網站。其他Web標准也有類似的問題:簡單地在瀏覽器中添加新方法就會破壞現有的站點。然而,這種破壞主要是由所謂的動態作用域(dynamic scope)造成的,所以ES6引入了一種特殊的符號symbol.unscopables, Web標准可以使用它來防止某些方法卷入動態作用域。

支持新的字符串匹配方式

在ES5中,str.match(myObject)會試圖把myObject轉換為RegExp(正則表達式)。在ES6中,JS首先會檢查myObject是否有一個myObject[Symbol.match](str)方法。現在JS庫可以提供自定義字符串解析類,這些類可以在RegExp對象工作的所有地方工作。

每一種用途都很小眾。這些特性本身很難對我的日常代碼產生重大影響。長遠的觀點更有趣。眾所周知的符號是JavaScript在PHP和Python中__doubleUnderscores下划線的改進版本。該標准將來將使用它們向語言中添加新的鈎子,而不會對現有代碼造成風險。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM