關(guān)于 ES6 的規(guī)范,官方命名為 ECMA-262 第六版本。采用的 ECMAScript 2015 語(yǔ)言規(guī)范,清除了最后的障礙并得到了 Ecma 組織的認(rèn)可。祝賀 TC39 和每個(gè)為 ES6 規(guī)范做出貢獻(xiàn)的人。這本書(shū)就講解了 ES6 !
更好的消息:下一個(gè)版本更新將不會(huì)再是六年之后。標(biāo)準(zhǔn)協(xié)會(huì)計(jì)劃在十二個(gè)月內(nèi)產(chǎn)生一個(gè)新版本。關(guān)于第七版本已經(jīng)在開(kāi)發(fā)中。
現(xiàn)在是時(shí)候來(lái)慶祝關(guān)于 JS 的最新版本,并且我仍然認(rèn)為 JS 的未來(lái)還有很大提升。
JS 與其它編程語(yǔ)言不太一樣,有時(shí)候它能以一種意想不到的方式在影響其他語(yǔ)言的發(fā)展。
ES6 模塊就是一個(gè)很好的例子。其他語(yǔ)言也有模塊系統(tǒng)。Racket 語(yǔ)言,Python 語(yǔ)言都有。當(dāng)標(biāo)準(zhǔn)協(xié)會(huì)決定加入模塊到 ES6 當(dāng)中,為什么不直接復(fù)制已經(jīng)存在的模塊系統(tǒng)呢?
JS 是不一樣的,因?yàn)樗侵苯釉?web 瀏覽器上運(yùn)行。I/O 操作將耗費(fèi)很長(zhǎng)的時(shí)間。這樣, JS 需要一個(gè)提供異步支持的模塊系統(tǒng)。它不能忍受在多個(gè)目錄下搜索模塊。顯然,復(fù)制已有的系統(tǒng)是不可取的。這樣 ES6 模塊系統(tǒng)需要一些新的東西。
關(guān)于如何影響最終的設(shè)計(jì)是一個(gè)有趣的故事。但是這里我們不討論。
這一部分關(guān)于 ES6 標(biāo)準(zhǔn)被稱為 “鍵值的容器” :Set,Map,WeakSet,和 WeakMap。在大多數(shù)方面,這些 “鍵值容器” 的特征類似其他語(yǔ)言的哈希表。但是標(biāo)準(zhǔn)協(xié)做了一些有趣的事來(lái)權(quán)衡 “鍵值容器” 和哈希表,因?yàn)?JS 是與眾不同的。
任何一個(gè)對(duì) JS 熟悉的人都知道 JS 中已經(jīng)存在和哈希表類似的結(jié)構(gòu): objects 。
一個(gè)純對(duì)象畢竟只是一個(gè)開(kāi)放式的鍵-值對(duì)的集合。你可以獲得,設(shè)置,和刪除屬性,或者迭代——所有哈希表可以做的操作。因此,為什么需要添加一個(gè)新的特征呢?
許多程序用純對(duì)象來(lái)存儲(chǔ)鍵-值對(duì),并且在程序運(yùn)行很好的情況下,沒(méi)有特殊原因的選擇 Map 或者 Set 。這里仍然存在一些關(guān)于使用對(duì)象(objects )所存在的眾所周知的問(wèn)題:
ES6 增加了新的考慮:純對(duì)象是不可迭代的,因此它們將無(wú)法用 for-of 循環(huán)或者 ... 等操作符。
大量的程序中都認(rèn)為純對(duì)象是一個(gè)不錯(cuò)的選擇。 Map 和 Set 作為其他情況下的選擇。
因?yàn)樗鼈儽挥脕?lái)避免用戶數(shù)據(jù)和內(nèi)置方法之間的沖突,在 ES6 容器中不用公開(kāi)它們的數(shù)據(jù)屬性。這就意味著像 obj.key 或者 obj[key] 將不能被用來(lái)訪問(wèn)哈希表數(shù)據(jù)。你就只有寫(xiě) map.get(key) 了。通常,哈希表實(shí)體不像屬性一樣,不能通過(guò)原型鏈被繼承。
不像純對(duì)象一樣, Map 和 Set 提供了方法,很多方法都能在標(biāo)準(zhǔn)類或之類當(dāng)中被添加,而且沒(méi)有沖突。
一個(gè) Set 是一組值的集合。該集合是可變的,所以你可以通過(guò)編程來(lái)增加或者刪除值。到目前為止, Set 就像是一個(gè)數(shù)組。盡管 Set 和數(shù)組如此的相似,但是他們兩者之間同樣存在許多差異。
首先,不像數(shù)組,一個(gè) Set不允許有重復(fù)的值。如果你試圖增加一個(gè) Set 中已經(jīng)存在的值,那么你等于什么都沒(méi)有做。
> var desserts = new Set("1","2","3","4");
> desserts.size
4
> desserts.add("1");
Set [ "1", "2", "3", "4" ]
> desserts.size
4
這個(gè)例子用到了字符, Set 能夠包含 JS 提供的任意類型值。 當(dāng)只用字符串時(shí),當(dāng)你添加已經(jīng)在 Set 中存在的字符串時(shí),對(duì)于 Set 來(lái)說(shuō)沒(méi)有任何效果。
其次, 一個(gè) Set 會(huì)有效的組織它的數(shù)據(jù)以便可以高效的進(jìn)行操作:
> // Check whether "zythum" is a word.
> arrayOfWords.indexOf("zythum") !== -1 // slow
true
> setOfWords.has("zythum") // fast
true
你不能根據(jù)索引獲得 Set 中的值:
> arrayOfWords[15000]
"anapanapa"
> setOfWords[15000] // sets don't support indexing
undefined
下面給出關(guān)于 Set 的所有操作:
for (let value of set)
f(value, value, set);
這個(gè)方法和數(shù)組的 .forEach( ) 類似。
在所有的這些特性中,new Set(iterable) 構(gòu)造器就像一個(gè)發(fā)電室一樣,因?yàn)樗谡麄€(gè)數(shù)據(jù)結(jié)構(gòu)上操作。你可以用它將一個(gè)數(shù)組轉(zhuǎn)換為一個(gè) Set, 用一行代碼就可以刪除重復(fù)的值?;蛘撸梢允占a(chǎn)生的值來(lái)構(gòu)造 Set 。這個(gè)構(gòu)造器也復(fù)制了現(xiàn)有的 Set 。
我上周抱怨 ES6 的新的容器?,F(xiàn)在就開(kāi)始了。盡管 Set 很便利,但是有些漏掉的方法能使未來(lái)的標(biāo)準(zhǔn)更好:
這里有個(gè)好消息是上面所有的方法都已經(jīng)在 ES6 中實(shí)現(xiàn)了。
一個(gè) Map 是含有鍵-值對(duì)的集合。下面給出 Map 能干什么:
for (let [key, value] of map)
f(value, key, map);
臨時(shí)的參數(shù)順序可以類比于 Array.prototype.forEach( ) 方法。
這里我們又抱怨什么呢?這里有些我認(rèn)為有用的特性,但是在 ES6 中沒(méi)有提到:
這些特性都很容易添加。
OK。記得這一章節(jié)的時(shí)候我們就就埋下一個(gè)疑問(wèn),在瀏覽器中運(yùn)行這種獨(dú)特的形式到底是如何影響 JS 語(yǔ)言特性的設(shè)計(jì)呢?這里,我們將來(lái)揭開(kāi)謎底。我準(zhǔn)備了三個(gè)例子。這里先看前面兩個(gè)。
我可以告訴你,ES6 中有個(gè)有用的特性,那就是容器類不支持所有的類型。
假設(shè)我們有一個(gè)關(guān)于 URL 對(duì)象的 Set。
var urls = new Set;
urls.add(new URL(location.href)); // two URL objects.
urls.add(new URL(location.href)); // are they the same?
alert(urls.size); // 2
上面的兩個(gè) URL 對(duì)象都應(yīng)該被視為等價(jià)的。它們有著同樣的域。但是在 JavaScript 中,這兩個(gè)對(duì)象是有區(qū)別的,并且沒(méi)有任何方式可以重載語(yǔ)言概念上的相等。所以上面的例子中最后輸出的 alert(urls.size) 是 2。
其他的語(yǔ)言支持這種語(yǔ)言概念上的相等性。 Java ,Python,和 Ruby,每個(gè)類都能支持重載相等。在許多方案的實(shí)現(xiàn)中,哈希表可以使用不同的相等關(guān)系來(lái)創(chuàng)建。 C++ 支持兩者。
然而,上面的所有機(jī)制都需要用戶實(shí)現(xiàn)傳統(tǒng)的哈希函數(shù)和所有顯示系統(tǒng)的默認(rèn)哈希函數(shù)。標(biāo)準(zhǔn)協(xié)會(huì)選擇不公開(kāi) JS 的哈希代碼——至少,不是現(xiàn)在——由于關(guān)于互用和安全的開(kāi)放性問(wèn)題,暫時(shí)沒(méi)有公開(kāi) JS 的哈希代碼。
你可能認(rèn)為來(lái)自計(jì)算機(jī)的確定性行為很難讓人感到驚奇。但是當(dāng)我告訴開(kāi)發(fā)者后,Map 和 Set 迭代訪問(wèn)條目的順序是你們插入到容器的順序,他們通常會(huì)感到驚奇。訪問(wèn)容器的順序是確定性的。
我們習(xí)慣于認(rèn)為哈希表是任意的。我們已經(jīng)習(xí)慣接受這樣的認(rèn)識(shí)。但是避免這種任意性是有原因的。正如我在2012年所寫(xiě)的一樣:
這些都在 2012 年 2 月被討論到,我贊成任意迭代順序。然后我用實(shí)驗(yàn)來(lái)展示了保持插入順序?qū)?huì)使得哈希表操作很慢。我寫(xiě)了少量的 C++ 微基準(zhǔn)。結(jié)果令我很吃驚。
這樣,我們最終在 JS 中跟蹤哈希表的插入順序。
上周,我們討論一個(gè)涉及到 JS 動(dòng)畫(huà)庫(kù)的例子。我們?cè)噲D在每個(gè) DOM 對(duì)象中存儲(chǔ)一個(gè)布爾類型的 flag ,就像下面這樣:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
不幸的是,在一個(gè) DOM 對(duì)象上設(shè)置屬性是一個(gè)糟糕的想法,原稿中有討論原因。
原稿展示了如何用符號(hào)( symbols )來(lái)解決這個(gè)問(wèn)題。但是我們不能用 Set 來(lái)解決么?可以像下面這樣:
if (movingSet.has(element)) {
smoothAnimations(element);
}
movingSet.add(element);
這樣做的方法有個(gè)缺點(diǎn):Map 和 Set 對(duì)象會(huì)對(duì)每個(gè)鍵和值保持一個(gè)強(qiáng)引用。這就意味著如果一個(gè) DOM 元素從文檔中除去,垃圾收集器不能恢復(fù)內(nèi)存直到從 movingSet 中除去。
庫(kù)函數(shù)雖然取得了成功,但強(qiáng)加給用戶使用后要清除的操作。所以這樣如果用戶忘了清除,容易造成內(nèi)存溢出。
ES6 對(duì)此提供了一個(gè)令人驚訝的修復(fù)。把 movingSet 變成一個(gè) WeakSet ,而不是一個(gè) Set。內(nèi)存溢出就會(huì)解決!
這意味著可以用弱容器或者是符號(hào)來(lái)解決這一問(wèn)題。那么哪一個(gè)更好呢?關(guān)于討論哪一種方法更好,需要很長(zhǎng)的篇幅。如果你在 web 頁(yè)面都只用一種符號(hào)完成所有操作,那么這樣做也可行。如果你需要用多個(gè)符號(hào)來(lái)完成,那么這樣做是有危險(xiǎn)的。你最好考慮用弱容器來(lái)完成。
WeakMap 和 WeakSet 在行為上有點(diǎn)像 Map 和 Set , 但是有些限制:
注意到弱容器是不能迭代的。
這些精心設(shè)計(jì)的限制使得垃圾收集器能收集到死亡的對(duì)象。效果類似于你獲得弱引用或者一個(gè)弱鍵字典,但是 ES6 的弱容器使得內(nèi)存管理可以在不揭示 GC 情況下完成,這是大有益處的。
簡(jiǎn)而言之, 一個(gè) WeakSet 對(duì)于它所包含的對(duì)象沒(méi)有一個(gè)強(qiáng)引用。當(dāng)你的 WeakSet 收集到一個(gè)對(duì)象的時(shí)候,該對(duì)象很容易被 WeakSet 刪除。WeakMap 是同樣的道理。它也不需要維護(hù)一個(gè)鍵的強(qiáng)引用。如果一個(gè)鍵時(shí)可用的,引用就是可用的。
為什么要接受這樣的限制呢?為什么要在 JS 中增加弱引用呢?
標(biāo)準(zhǔn)協(xié)會(huì)很不情愿揭示腳本的不確定性。性能不好的瀏覽器是 Web 發(fā)展的障礙。弱引用揭示了潛在垃圾收集器的實(shí)現(xiàn)細(xì)節(jié)——平臺(tái)特有的任意行為。當(dāng)然,應(yīng)用不依賴于平臺(tái)的實(shí)現(xiàn)細(xì)節(jié),但是弱引用使得我們很難知道我們對(duì)于瀏覽器有多依賴。這里很難解釋清楚。
與之相反的是, ES6 的弱容器有一些限制的特征集,但是這些特征固定的。事實(shí)上一個(gè)鍵或者一個(gè)值被收集是很難被觀察到的,因此這些應(yīng)用不能依賴于瀏覽器。
這是一個(gè)基于瀏覽器考慮的例子,從而引出了一個(gè)令人驚訝的設(shè)計(jì)決策,使 JS 成為更好的語(yǔ)言。
四種容器類目前已在 Firefox ,Chrome, Microsoft Edge 和 Safari 中實(shí)現(xiàn)。為了支持老的瀏覽器,需要用 es6-collections 工具來(lái)輔助。
在 Firefox 中, WeakMap 最早由其首席技術(shù)官 Andreas Gal, 實(shí)現(xiàn)。 Tom Schuster 實(shí)現(xiàn)了 WeakSet 。我實(shí)現(xiàn)了 Map 和 Set 。感謝 Tooru Fujisawa 提供的補(bǔ)丁。