第七種類型
自從JavaScript在1997年首次標准化以來,已經有了六種類型。在ES6之前,JS程序中的每個值都屬於這些類別之一:
- Undefined
- Null
- Boolean
- Number
- String
- Object
每種類型都是一組值。前五個集合都是有限的。當然,只有兩個布爾值,true
和false
,而且它們不會產生新的值。有更多的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-in
或Object.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
下划線的改進版本。該標准將來將使用它們向語言中添加新的鈎子,而不會對現有代碼造成風險。