# 第八章:ES6以后
在本書寫作的時候,ES6(*ECMAScript 2015*)的最終草案即將為了ECMA的批準而進行最終的官方投票。但即便是在ES6已經被最終定稿的時候,TC39協會已經在為了ES7/2016和將來的特性進行努力的工作。
正如我們在第一章中討論過的,預計JS進化的節奏將會從好幾年升級一次加速到每年進行一次官方的版本升級(因此采用編年命名法)。這將會徹底改變JS開發者學習與跟上這門語言腳步的方式。
但更重要的是,協會實際上將會一個特性一個特性地進行工作。只要一種特性的規范被定義完成,而且通過在幾種瀏覽器中的實驗性實現打通了關節,那么這種特性就會被認為足夠穩定并可以開始使用了。我們都被強烈鼓勵一旦特性準備好就立即采用它,而不是等待什么官方標準投票。如果你還沒學過ES6,現在上船的日子已經過了!
在本書寫作時,一個未來特性提案的列表和它們的狀態可以在這里看到(https://github.com/tc39/ecma262#current-proposals)。
在所有我們支持的瀏覽器實現這些新特性之前,轉譯器和填補是我們如何橋接它們的方法。Babel,Traceur,和其他幾種主流轉譯器已經支持了一些最可能穩定下來的ES6之后的特性。
認識到這一點,是時候看一看它們之中的一些了。讓我們開始吧!
警告:?這些特性都處于開發的各種階段。雖然它們很可能確定下來,而且將與本章的內容看起來相似,但還是要抱著更多質疑的態度看待本章的內容。這一章將會在本書未來的版本中隨著這些(和其他的!)特性的確定而演化。
## `async function`
我們在第四章的“Generators + Promises”中提到過,generator`yield`一個promise給一個類似運行器的工具,它會在promise完成時推進generator —— 有一個提案是要為這種模式提供直接的語法支持。讓我們簡要看一下這個被提出的特性,它稱為`async function`。
回想一下第四章中的這個generator的例子:
```source-js
run( function *main() {
var ret = yield step1();
try {
ret = yield step2( ret );
}
catch (err) {
ret = yield step2Failed( err );
}
ret = yield Promise.all([
step3a( ret ),
step3b( ret ),
step3c( ret )
]);
yield step4( ret );
} )
.then(
function fulfilled(){
// `*main()` 成功地完成了
},
function rejected(reason){
// 噢,什么東西搞錯了
}
);
```
被提案的`async function`語法可以無需`run(..)`工具就表達相同的流程控制邏輯,因為JS將會自動地知道如何尋找promise來等待和推進。考慮如下代碼:
```source-js
async function main() {
var ret = await step1();
try {
ret = await step2( ret );
}
catch (err) {
ret = await step2Failed( err );
}
ret = await Promise.all( [
step3a( ret ),
step3b( ret ),
step3c( ret )
] );
await step4( ret );
}
main()
.then(
function fulfilled(){
// `main()` 成功地完成了
},
function rejected(reason){
// 噢,什么東西搞錯了
}
);
```
取代`function *main() { ..`聲明的,是我們使用`async function main() { ..`形式聲明。而取代`yield`一個promise的,是我們`await`這個promise。運行`main()`函數的調用實際上返回一個我們可以直接監聽的promise。這與我們從一個`run(main)`調用中拿回一個promise是等價的。
你看到對稱性了嗎?`async function`實質上是 generators + promises +?`run(..)`模式的語法糖;它們在底層的操作是相同的!
如果你是一個C#開發者而且這種`async`/`await`看起來很熟悉,那是因為這種特性就是直接由C#的特性啟發的。看到語言提供一致性是一件好事!
Babel、Traceur 以及其他轉譯器已經對當前的`async function`狀態有了早期支持,所以你已經可以使用它們了。但是,在下一節的“警告”中,我們將看到為什么你也許還不應該上這艘船。
注意:?還有一個`async function*`的提案,它應當被稱為“異步generator”。你可以在同一段代碼中使用`yield`和`await`兩者,甚至是在同一個語句中組合這兩個操作:`x = await yield y`。“異步generator”提案看起來更具變化 —— 也就是說,它返回一個沒有還沒有完全被計算好的值。一些人覺得它應當是一個?*可監聽對象(observable)*,有些像是一個迭代器和promise的組合。就目前來說,我們不會進一步探討這個話題,但是會繼續關注它的演變。
### 警告
關于`async function`的一個未解的爭論點是,因為它僅返回一個promise,所以沒有辦法從外部?*撤銷*?一個當前正在運行的`async function`實例。如果這個異步操作是資源密集型的,而且你想在自己確定不需要它的結果時能立即釋放資源,這可能是一個問題。
舉例來說:
```source-js
async function request(url) {
var resp = await (
new Promise( function(resolve,reject){
var xhr = new XMLHttpRequest();
xhr.open( "GET", url );
xhr.onreadystatechange = function(){
if (xhr.readyState == 4) {
if (xhr.status == 200) {
resolve( xhr );
}
else {
reject( xhr.statusText );
}
}
};
xhr.send();
} )
);
return resp.responseText;
}
var pr = request( "http://some.url.1" );
pr.then(
function fulfilled(responseText){
// ajax 成功
},
function rejected(reason){
// 噢,什么東西搞錯了
}
);
```
我構想的`request(..)`有點兒像最近被提案要包含進web平臺的`fetch(..)`工具。我們關心的是,例如,如果你想要用`pr`值以某種方法指示撤銷一個長時間運行的Ajax請求會怎么樣?
Promise是不可撤銷的(在本書寫作時)。在我和其他許多人看來,它們就不應該是可以被撤銷的(參見本系列的?*異步與性能*)。而且即使一個proimse確實擁有一個`cancel()`方法,那么一定意味著調用`pr.cancel()`應當真的沿著promise鏈一路傳播一個撤銷信號到`async function`嗎?
對于這個爭論的幾種可能的解決方案已經浮出水面:
* `async function`將根本不能被撤銷(現狀)
* 一個“撤銷存根”可以在調用時傳遞給一個異步函數
* 將返回值改變為一個新增的可撤銷promsie類型
* 將返回值改變為非promise的其他東西(比如,可監聽對象,或帶有promise和撤銷能力的控制存根)
在本書寫作時,`async function`返回普通的promise,所以完全改變返回值不太可能。但是現在下定論還是為時過早了。讓我們持續關注這個討論吧。
## `Object.observe(..)`
前端web開發的圣杯之一就是數據綁定 —— 監聽一個數據對象的更新并同步這個數據的DOM表現形式。大多數JS框架都為這些類型的操作提供某種機制。
在ES6后期,我們似乎很有可能看到這門語言通過一個稱為`Object.observe(..)`的工具,對此提供直接的支持。實質上,它的思想是你可以建立監聽器來監聽一個對象的變化,并在一個變化發生的任何時候調用一個回調。例如,你可相應地更新DOM。
你可以監聽六種類型的變化:
* add
* update
* delete
* reconfigure
* setPrototype
* preventExtensions
默認情況下,你將會收到所有這些類型的變化的通知,但是你可以將它們過濾為你關心的那一些。
考慮如下代碼:
```source-js
var obj = { a: 1, b: 2 };
Object.observe(
obj,
function(changes){
for (var change of changes) {
console.log( change );
}
},
[ "add", "update", "delete" ]
);
obj.c = 3;
// { name: "c", object: obj, type: "add" }
obj.a = 42;
// { name: "a", object: obj, type: "update", oldValue: 1 }
delete obj.b;
// { name: "b", object: obj, type: "delete", oldValue: 2 }
```
除了主要的`"add"`、`"update"`、和`"delete"`變化類型:
* `"reconfigure"`變化事件在對象的一個屬性通過`Object.defineProperty(..)`而重新配置時觸發,比如改變它的`writable`屬性。更多信息參見本系列的?*this與對象原型*。
* `"preventExtensions"`變化事件在對象通過`Object.preventExtensions(..)`被設置為不可擴展時觸發。
因為`Object.seal(..)`和`Object.freeze(..)`兩者都暗示著`Object.preventExtensions(..)`,所以它們也將觸發相應的變化事件。另外,`"reconfigure"`變化事件也會為對象上的每個屬性被觸發。
* `"setPrototype"`變化事件在一個對象的`[[Prototype]]`被改變時觸發,不論是使用`__proto__`setter,還是使用`Object.setPrototypeOf(..)`設置它。
注意,這些變化事件在會在變化發生后立即觸發。不要將它們與代理(見第七章)搞混,代理是可以在動作發生之前攔截它們的。對象監聽讓你在變化(或一組變化)發生之后進行應答。
### 自定義變化事件
除了六種內建的變化事件類型,你還可以監聽并觸發自定義變化事件。
考慮如下代碼:
```source-js
function observer(changes){
for (var change of changes) {
if (change.type == "recalc") {
change.object.c =
change.object.oldValue +
change.object.a +
change.object.b;
}
}
}
function changeObj(a,b) {
var notifier = Object.getNotifier( obj );
obj.a = a * 2;
obj.b = b * 3;
// queue up change events into a set
notifier.notify( {
type: "recalc",
name: "c",
oldValue: obj.c
} );
}
var obj = { a: 1, b: 2, c: 3 };
Object.observe(
obj,
observer,
["recalc"]
);
changeObj( 3, 11 );
obj.a; // 12
obj.b; // 30
obj.c; // 3
```
變化的集合(`"recalc"`自定義事件)為了投遞給監聽器而被排隊,但還沒被投遞,這就是為什么`obj.c`依然是`3`。
默認情況下,這些變化將在當前事件輪詢(參見本系列的?*異步與性能*)的末尾被投遞。如果你想要立即投遞它們,使用`Object.deliverChangeRecords(observer)`。一旦這些變化投遞完成,你就可以觀察到`obj.c`如預期地更新為:
```source-js
obj.c; // 42
```
在前面的例子中,我們使用變化完成事件的記錄調用了`notifier.notify(..)`。將變化事件的記錄進行排隊的一種替代形式是使用`performChange(..)`,它把事件的類型與事件記錄的屬性(通過一個函數回調)分割開來。考慮如下代碼:
```source-js
notifier.performChange( "recalc", function(){
return {
name: "c",
// `this` 是被監聽的對象
oldValue: this.c
};
} );
```
在特定的環境下,這種關注點分離可能與你的使用模式匹配的更干凈。
### 中止監聽
正如普通的事件監聽器一樣,你可能希望停止監聽一個對象的變化事件。為此,你可以使用`Object.unobserve(..)`。
舉例來說:
```source-js
var obj = { a: 1, b: 2 };
Object.observe( obj, function observer(changes) {
for (var change of changes) {
if (change.type == "setPrototype") {
Object.unobserve(
change.object, observer
);
break;
}
}
} );
```
在這個小例子中,我們監聽變化事件直到我們看到`"setPrototype"`事件到來,那時我們就不再監聽任何變化事件了。
## 指數操作符
為了使JavaScript以與`Math.pow(..)`相同的方式進行指數運算,有一個操作符被提出了。考慮如下代碼:
```source-js
var a = 2;
a ** 4; // Math.pow( a, 4 ) == 16
a **= 3; // a = Math.pow( a, 3 )
a; // 8
```
注意:?`**`實質上在Python、Ruby、Perl、和其他語言中都與此相同。
## 對象屬性與?`...`
正如我們在第二章的“太多,太少,正合適”一節中看到的,`...`操作符在擴散或收集一個數組上的工作方式是顯而易見的。但對象會怎么樣?
這樣的特性在ES6中被考慮過,但是被推遲到ES6之后(也就是“ES7”或者“ES2016”或者……)了。這是它在“ES6以后”的時代中可能的工作方式:
```source-js
var o1 = { a: 1, b: 2 },
o2 = { c: 3 },
o3 = { ...o1, ...o2, d: 4 };
console.log( o3.a, o3.b, o3.c, o3.d );
// 1 2 3 4
```
`...`操作符也可能被用于將一個對象的被解構屬性收集到另一個對象:
```source-js
var o1 = { b: 2, c: 3, d: 4 };
var { b, ...o2 } = o1;
console.log( b, o2.c, o2.d ); // 2 3 4
```
這里,`...o2`將被解構的`c`和`d`屬性重新收集到一個`o2`對象中(與`o1`不同,`o2`沒有`b`屬性)。
重申一下,這些只是正在考慮之中的ES6之后的提案。但是如果它們能被確定下來就太酷了。
## `Array#includes(..)`
JS開發者需要執行的極其常見的一個任務就是在一個值的數組中搜索一個值。完成這項任務的方式曾經總是:
```source-js
var vals = [ "foo", "bar", 42, "baz" ];
if (vals.indexOf( 42 ) >= 0) {
// 找到了!
}
```
進行`>= 0`檢查是因為`indexOf(..)`在找到結果時返回一個`0`或更大的數字值,或者在沒找到結果時返回`-1`。換句話說,我們在一個布爾值的上下文環境中使用了一個返回索引的函數。而由于`-1`是truthy而非falsy,所以我們不得不手動進行檢查。
在本系列的?*類型與文法*?中,我探索了另一種我稍稍偏好的模式:
```source-js
var vals = [ "foo", "bar", 42, "baz" ];
if (~vals.indexOf( 42 )) {
// 找到了!
}
```
這里的`~`操作符使`indexOf(..)`的返回值與一個值的范圍相一致,這個范圍可以恰當地強制轉換為布爾型。也就是,`-1`產生`0`(falsy),而其余的東西產生非零值(truthy),而這正是我們判定是否找到值的依據。
雖然我覺得這是一種改進,但有另一些人強烈反對。然而,沒有人會質疑`indexOf(..)`的檢索邏輯是完美的。例如,在數組中查找`NaN`值會失敗。
于是一個提案浮出了水面并得到了大量的支持 —— 增加一個真正的返回布爾值的數組檢索方法,稱為`includes(..)`:
```source-js
var vals = [ "foo", "bar", 42, "baz" ];
if (vals.includes( 42 )) {
// 找到了!
}
```
注意:?`Array#includes(..)`使用了將會找到`NaN`值的匹配邏輯,但將不會區分`-0`與`0`(參見本系列的?*類型與文法*)。如果你在自己的程序中不關心`-0`值,那么它很可能正是你希望的。如果你?*確實*?關心`-0`,那么你就需要實現你自己的檢索邏輯,很可能是使用`Object.is(..)`工具(見六章)。
## SIMD
我們在本系列的?*異步與性能*?中詳細講解了一個指令,多個數據(SIMD),但因為它是未來JS中下一個很可能被確定下來的特性,所以這里簡要地提一下。
SIMD API 暴露了各種底層(CPU)指令,它們可以同時操作一個以上的數字值。例如,你可以指定兩個擁有4個或8個數字的?*向量*,然后一次性分別相乘所有元素(數據并行機制!)。
考慮如下代碼:
```source-js
var v1 = SIMD.float32x4( 3.14159, 21.0, 32.3, 55.55 );
var v2 = SIMD.float32x4( 2.1, 3.2, 4.3, 5.4 );
SIMD.float32x4.mul( v1, v2 );
// [ 6.597339, 67.2, 138.89, 299.97 ]
```
SIMD將會引入`mul(..)`(乘法)之外的幾種其他操作,比如`sub()`、`div()`、`abs()`、`neg()`、`sqrt()`、以及其他許多。
并行數學操作對下一代的高性能JS應用程序至關重要。
## WebAssembly (WASM)
在本書的第一版將近完成的時候,Brendan Eich 突然宣布了一個有可能對JavaScript未來的道路產生重大沖擊的公告:WebAssembly(WASM)。我們不能在這里詳細地探討WASM,因為在本書寫作時這個話題為時過早了。但如果不簡要地提上一句,這本書就不夠完整。
JS語言在近期(和近未來的)設計的改變上所承受的最大壓力之一,就是渴望它能夠成為從其他語言(比如 C/C++,ClojureScript,等等)轉譯/交叉編譯來的、合適的目標語言。顯然,作為JavaScript運行的代碼性能是一個主要問題。
正如在本系列的?*異步與性能*?中討論過的,幾年前一組在Mozilla的開發者給JavaScript引入了一個稱為ASM.js的想法。AMS.js是一個合法JS的子集,它大幅地制約了使代碼難于被JS引擎優化的特定行為。其結果就是兼容AMS.js的代碼在一個支持ASM的引擎上可以顯著地快速運行,幾乎可以與優化過的原生C語言的等價物相媲美。許多觀點認為,對于那些將要由JavaScript編寫的渴求性能的應用程序來說,ASM.js很可能將是它們的基干。
換言之,在瀏覽器中條條大路通過JavaScript通向運行的代碼。
直到WASM公告之前,是這樣的。WASM提供了另一條路線,讓其他語言不必非得首先通過JavaScript就能將瀏覽器的運行時環境作為運行的目標。實質上,如果WASM啟用,JS引擎將會生長出額外的能力 —— 執行可以被視為有些與字節碼相似的二進制代碼(就像在JVM上運行的那些東西)。
WASM提出了一種高度壓縮的代碼AST(語法樹)的二進制表示格式,它可以繼而像JS引擎以及它的基礎結構直接發出指令,無需被JS解析,甚至無需按照JS的規則動作。像C或C++這樣的語言可以直接被編譯為WASM格式而非ASM.js,并且由于跳過JS解析而得到額外的速度優勢。
短期內,WASM與AMS.js、JS不相上下。但是最終,人們預期WASM將會生長出新的能力,那將超過JS能做的任何事情。例如,讓JS演化出像線程這樣的根本特性 —— 一個肯定會對JS生態系統造成重大沖擊的改變 —— 作為一個WASM未來的擴展更有希望,也會緩解改變JS的壓力。
事實上,這張新的路線圖為許多語言服務于web運行時開啟了新的道路。對于web平臺來說,這真是一個激動人心的新路線!
它對JS意味著什么?JS將會變得無關緊要或者“死去”嗎?絕對不是。ASM.js在接下來的幾年中很可能看不到太多未來,但JS在數量上的絕對優勢將它安全地錨定在web平臺中。
WASM的擁護者們說,它的成功意味著JS的設計將會被保護起來,遠離那些最終會迫使它超過自己合理性的臨界點的壓力。人們估計WASM將會成為應用程序中高性能部分的首選目標語言,這些部分曾用各種各樣不同的語言編寫過。
有趣的是,JavaScript是未來不太可能以WASM為目標的語言之一。可能有一些未來的改變會切出JS的一部分,而使這一部分更適于以WASM作為目標,但是這件事情看起來優先級不高。
雖然JS很可能與WASM沒什么關聯,但JS代碼和WASM代碼將能夠以最重要的方式進行交互,就像當下的模塊互動一樣自然。你可以想象,調用一個`foo()`之類的JS函數而使它實際上調用一個同名WASM函數,它具備遠離你其余JS的制約而運行的能力。
至少是在可預見的未來,當下以JS編寫的東西可能將繼續總是由JS編寫。轉譯為JS的東西將可能最終至少考慮以WASM為目標。對于那些需要極致性能,而且在抽象的層面上沒有余地的東西,最有可能的選擇是找一種合適的非JS語言編寫,然后以WASM為目標語言。
這個轉變很有可能將會很慢,會花上許多年成形。WASM在所有的主流瀏覽器上固定下來可能最快也要花幾年。同時,WASM項目([https://github.com/WebAssembly)有一個早期填補,來為它的基本原則展示概念證明。](https://github.com/WebAssembly)
但隨著時間的推移,也隨著WASM學到新的非JS技巧,不難想象一些當前是JS的東西被重構為以WASM作為目標的語言。例如,框架中性能敏感的部分,游戲引擎,和其他被深度使用的工具都很可能從這樣的轉變中獲益。在web應用程序中使用這些工具的開發者們并不會在使用或整合上注意到太多不同,但確實會自動地利用這些性能和能力。
可以確定的是,隨著WASM變得越來越真實,它對JavaScript設計路線的影響就越來越多。這可能是開發者們應當關注的最重要的“ES6以后”的話題。
## 復習
如果這個系列的其他書目實質上提出了這個挑戰,“你(可能)不懂JS(不像自己想象的那么懂)”,那么這本書就是在說,“你不再懂JS了”。這本書講解了在ES6中加入到語言里的一大堆新東西。它是一個新語言特性的精彩集合,也是將永遠改進我們JS程序的范例。
但JS不是到ES6就完了!還早得很呢。已經有好幾個“ES6之后”的特性處于開發的各個階段。在這一章中,我們簡要地看了一些最有可能很快會被固定在JS中的候選特性。
`async function`是建立在 generators + promises 模式(見第四章)上的強大語法糖。`Object.observe(..)`為監聽對象變化事件增加了直接原生的支持,它對實現數據綁定至關重要。`**`指數作符,針對對象屬性的`...`,以及`Array#includes(..)`都是對現存機制的簡單而有用的改進。最后,SIMD將高性能JS的演化帶入一個新紀元。
聽起來很俗套,但JS的未來是非常光明的!這個系列,以及這本書的挑戰,現在是各位讀者的職責了。你還在等什么?是時候開始學習和探索了!