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 來探測元素是否處於動畫狀態,但殺雞焉用牛刀,在將元素設置為移動時,你的庫就知道了該元素正在移動。
你真正需要的是一種機制來跟蹤哪些元素正在移動,你可以將正在移動的元素保存在一個數組中,每次要為一個元素設置動畫時,首先檢查一下這個元素是否已經在這個列表中。
啊哈,但是如果你的數組非常龐大,即便是這樣的線性搜索也會產生性能問題。
那麼,你真正想做的就是直接在元素上設置一個標志:
if (element.isMoving) { smoothAnimations(element); } element.isMoving = true; if (element.isMoving) { smoothAnimations(element); } element.isMoving = true;
這也有一些潛在的問題,不得不承認這樣一個事實:還有其他代碼也可能操作該 ODM 元素。
當然,對於最後三個問題,你可以選擇一個無意義的不會有人會使用到的字符串:
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; 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; // get 1024 Unicode characters of gibberish var isMoving = SecureRandom.generateName(); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true;
object[name] 語法允許我們將任何字符串作為屬性名,代碼能正常工作,沖突幾乎是不可能了,代碼看起來也美觀多了。
但是,這回導致糟糕的調試體驗,每次使用 console.log() 打印出包含該屬性的元素時,你回看到一個龐大的垃圾字符串,並且如果還不止一個這樣的屬性呢?每次刷新後屬性名都發生了變化,怎麼樣使這些屬性看起來更加直觀呢?
為什麼這麼難?我們只是為了保存一個小小的標志位。
用 Symbol 來解決問題
Symbol 值可以由程序創建,並可以作為屬性名,而且不用擔心屬性名沖突。
var mySymbol = Symbol(); var mySymbol = Symbol();
調用 Symbol() 方法將創建一個新的 Symbol 類型的值,並且該值不與其它任何值相等。
與數字和字符串一樣,Symbol 類型的值也可以作為對象的屬性名,正是由於它不與任何其它值相等,對應的屬性也不會發生沖突:
obj[mySymbol] = "ok!"; // guaranteed not to collide console.log(obj[mySymbol]); // ok! obj[mySymbol] = "ok!"; // guaranteed not to collide console.log(obj[mySymbol]); // ok!
下面是使用 Symbol 來解決上面的問題:
// create a unique symbol var isMoving = Symbol("isMoving"); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true; // create a unique symbol var isMoving = Symbol("isMoving"); ... if (element[isMoving]) { smoothAnimations(element); } element[isMoving] = true;
上面代碼需要注意幾點:
由於 Symbol 的設計初衷是為了避免沖突,當遍歷 JavaScript 對象時,並不會枚舉到以 Symbol 作為建的屬性,比如,for-in 循環只會遍歷到以字符串作為鍵的屬性,Object.keys(obj)和 Object.getOwnPropertyNames(obj) 也一樣,但這並不意味著 Symbol 為鍵的屬性是不可枚舉的:使用 Object.getOwnPropertySymbols(obj) 這個新方法可以枚舉出來,還有 Reflect.ownKeys(obj) 這個新方法可以返回對象中所有字符串和 Symbol 鍵。(我將在後面的文章中詳細介紹 Reflect 這個新特性。)
庫和框架的設計者將會發現很多 Symbol 的用途,稍後我們將看到,JavaScript 語言本身也對其有廣泛的應用。
Symbol 究竟是什麼呢
> typeof Symbol() "symbol" > typeof Symbol() "symbol"
Symbol 是完全不一樣的東西。一旦創建後就不可更改,不能對它們設置屬性(如果在嚴格模式下嘗試這樣做,你將得到一個 TypeError)。它們可以作為屬性名,這時它們和字符串的屬性名沒有什麼區別。
另一方面,每個 Symbol 都是獨一無二的,不與其它 Symbol 重復(即便是使用相同的 Symbol 描述創建),創建一個 Symbol 就跟創建一個對象一樣方便。
ES6 中的 Symbol 與傳統語言(如 Lisp 和 Ruby)中的 Symbol 中的類似,但並不是完全照搬到 JavaScript 中。在 Lisp 中,所有標識符都是 Symbol;在 JavaScript 中,標識符和大多數屬性仍然是字符串,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 > 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 在 ES6 規范中的應用
我們已經知道可以使用 Symbol 來避免代碼沖突。之前在介紹 iterator 時,我們還解析了 for (var item of myArray) 內部是以調用 myArray[Symbol.iterator]() 開始的,當時我提到這個方法可以使用 myArray.iterator() 來代替,但是使用 Symbol 的後向兼容性更好。
在 ES6 中還有一些地方使用到了 Symbol。(這些特性還沒有在 FireFox 中實現。)
這些用途還比較窄,但僅僅通過我文章中的代碼很難看到這些新特性產生的重大影響。JavaScript 的 Symbol 是 PHP 和 Python 中 __doubleUnderscores 的改進版本,標准組織將使用它來為語言添加新特性,而不會對已有代碼產生影響。