特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS
在本書寫作的時候,ES6(ECMAScript 2015)的最終草案即將為了ECMA的批準而進行最終的官方投票。但即便是在ES6已經(jīng)被最終定稿的時候,TC39協(xié)會已經(jīng)在為了ES7/2016和將來的特性進行努力的工作。
正如我們在第一章中討論過的,預計JS進化的節(jié)奏將會從好幾年升級一次加速到每年進行一次官方的版本升級(因此采用編年命名法)。這將會徹底改變JS開發(fā)者學習與跟上這門語言腳步的方式。
但更重要的是,協(xié)會實際上將會一個特性一個特性地進行工作。只要一種特性的規(guī)范被定義完成,而且通過在幾種瀏覽器中的實驗性實現(xiàn)打通了關節(jié),那么這種特性就會被認為足夠穩(wěn)定并可以開始使用了。我們都被強烈鼓勵一旦特性準備好就立即采用它,而不是等待什么官方標準投票。如果你還沒學過ES6,現(xiàn)在上船的日子已經(jīng)過了!
在本書寫作時,一個未來特性提案的列表和它們的狀態(tài)可以在這里看到(https://github.com/tc39/ecma262#current-proposals)。
在所有我們支持的瀏覽器實現(xiàn)這些新特性之前,轉譯器和填補是我們如何橋接它們的方法。Babel,Traceur,和其他幾種主流轉譯器已經(jīng)支持了一些最可能穩(wěn)定下來的ES6之后的特性。
認識到這一點,是時候看一看它們之中的一些了。讓我們開始吧!
警告: 這些特性都處于開發(fā)的各種階段。雖然它們很可能確定下來,而且將與本章的內容看起來相似,但還是要抱著更多質疑的態(tài)度看待本章的內容。這一章將會在本書未來的版本中隨著這些(和其他的?。┨匦缘拇_定而演化。
async function
我們在第四章的“Generators + Promises”中提到過,generatoryield一個promise給一個類似運行器的工具,它會在promise完成時推進generator —— 有一個提案是要為這種模式提供直接的語法支持。讓我們簡要看一下這個被提出的特性,它稱為async function。
回想一下第四章中的這個generator的例子:
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來等待和推進??紤]如下代碼:
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()函數(shù)的調用實際上返回一個我們可以直接監(jiān)聽的promise。這與我們從一個run(main)調用中拿回一個promise是等價的。
你看到對稱性了嗎?async function實質上是 generators + promises + run(..)模式的語法糖;它們在底層的操作是相同的!
如果你是一個C#開發(fā)者而且這種async/await看起來很熟悉,那是因為這種特性就是直接由C#的特性啟發(fā)的??吹秸Z言提供一致性是一件好事!
Babel、Traceur 以及其他轉譯器已經(jīng)對當前的async function狀態(tài)有了早期支持,所以你已經(jīng)可以使用它們了。但是,在下一節(jié)的“警告”中,我們將看到為什么你也許還不應該上這艘船。
注意: 還有一個async function*的提案,它應當被稱為“異步generator”。你可以在同一段代碼中使用yield和await兩者,甚至是在同一個語句中組合這兩個操作:x = await yield y。“異步generator”提案看起來更具變化 —— 也就是說,它返回一個沒有還沒有完全被計算好的值。一些人覺得它應當是一個 可監(jiān)聽對象(observable),有些像是一個迭代器和promise的組合。就目前來說,我們不會進一步探討這個話題,但是會繼續(xù)關注它的演變。
警告
關于async function的一個未解的爭論點是,因為它僅返回一個promise,所以沒有辦法從外部 撤銷 一個當前正在運行的async function實例。如果這個異步操作是資源密集型的,而且你想在自己確定不需要它的結果時能立即釋放資源,這可能是一個問題。
舉例來說:
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嗎?
對于這個爭論的幾種可能的解決方案已經(jīng)浮出水面:
-
async function將根本不能被撤銷(現(xiàn)狀) - 一個“撤銷存根”可以在調用時傳遞給一個異步函數(shù)
- 將返回值改變?yōu)橐粋€新增的可撤銷promsie類型
- 將返回值改變?yōu)榉莗romise的其他東西(比如,可監(jiān)聽對象,或帶有promise和撤銷能力的控制存根)
在本書寫作時,async function返回普通的promise,所以完全改變返回值不太可能。但是現(xiàn)在下定論還是為時過早了。讓我們持續(xù)關注這個討論吧。
Object.observe(..)
前端web開發(fā)的圣杯之一就是數(shù)據(jù)綁定 —— 監(jiān)聽一個數(shù)據(jù)對象的更新并同步這個數(shù)據(jù)的DOM表現(xiàn)形式。大多數(shù)JS框架都為這些類型的操作提供某種機制。
在ES6后期,我們似乎很有可能看到這門語言通過一個稱為Object.observe(..)的工具,對此提供直接的支持。實質上,它的思想是你可以建立監(jiān)聽器來監(jiān)聽一個對象的變化,并在一個變化發(fā)生的任何時候調用一個回調。例如,你可相應地更新DOM。
你可以監(jiān)聽六種類型的變化:
- add
- update
- delete
- reconfigure
- setPrototype
- preventExtensions
默認情況下,你將會收到所有這些類型的變化的通知,但是你可以將它們過濾為你關心的那一些。
考慮如下代碼:
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(..)而重新配置時觸發(fā),比如改變它的writable屬性。更多信息參見本系列的 this與對象原型。-
"preventExtensions"變化事件在對象通過Object.preventExtensions(..)被設置為不可擴展時觸發(fā)。因為
Object.seal(..)和Object.freeze(..)兩者都暗示著Object.preventExtensions(..),所以它們也將觸發(fā)相應的變化事件。另外,"reconfigure"變化事件也會為對象上的每個屬性被觸發(fā)。 "setPrototype"變化事件在一個對象的[[Prototype]]被改變時觸發(fā),不論是使用__proto__setter,還是使用Object.setPrototypeOf(..)設置它。
注意,這些變化事件在會在變化發(fā)生后立即觸發(fā)。不要將它們與代理(見第七章)搞混,代理是可以在動作發(fā)生之前攔截它們的。對象監(jiān)聽讓你在變化(或一組變化)發(fā)生之后進行應答。
自定義變化事件
除了六種內建的變化事件類型,你還可以監(jiān)聽并觸發(fā)自定義變化事件。
考慮如下代碼:
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"自定義事件)為了投遞給監(jiān)聽器而被排隊,但還沒被投遞,這就是為什么obj.c依然是3。
默認情況下,這些變化將在當前事件輪詢(參見本系列的 異步與性能)的末尾被投遞。如果你想要立即投遞它們,使用Object.deliverChangeRecords(observer)。一旦這些變化投遞完成,你就可以觀察到obj.c如預期地更新為:
obj.c; // 42
在前面的例子中,我們使用變化完成事件的記錄調用了notifier.notify(..)。將變化事件的記錄進行排隊的一種替代形式是使用performChange(..),它把事件的類型與事件記錄的屬性(通過一個函數(shù)回調)分割開來。考慮如下代碼:
notifier.performChange( "recalc", function(){
return {
name: "c",
// `this` 是被監(jiān)聽的對象
oldValue: this.c
};
} );
在特定的環(huán)境下,這種關注點分離可能與你的使用模式匹配的更干凈。
中止監(jiān)聽
正如普通的事件監(jiān)聽器一樣,你可能希望停止監(jiān)聽一個對象的變化事件。為此,你可以使用Object.unobserve(..)。
舉例來說:
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;
}
}
} );
在這個小例子中,我們監(jiān)聽變化事件直到我們看到"setPrototype"事件到來,那時我們就不再監(jiān)聽任何變化事件了。
指數(shù)操作符
為了使JavaScript以與Math.pow(..)相同的方式進行指數(shù)運算,有一個操作符被提出了。考慮如下代碼:
var a = 2;
a ** 4; // Math.pow( a, 4 ) == 16
a **= 3; // a = Math.pow( a, 3 )
a; // 8
注意: **實質上在Python、Ruby、Perl、和其他語言中都與此相同。
對象屬性與 ...
正如我們在第二章的“太多,太少,正合適”一節(jié)中看到的,...操作符在擴散或收集一個數(shù)組上的工作方式是顯而易見的。但對象會怎么樣?
這樣的特性在ES6中被考慮過,但是被推遲到ES6之后(也就是“ES7”或者“ES2016”或者……)了。這是它在“ES6以后”的時代中可能的工作方式:
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
...操作符也可能被用于將一個對象的被解構屬性收集到另一個對象:
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開發(fā)者需要執(zhí)行的極其常見的一個任務就是在一個值的數(shù)組中搜索一個值。完成這項任務的方式曾經(jīng)總是:
var vals = [ "foo", "bar", 42, "baz" ];
if (vals.indexOf( 42 ) >= 0) {
// 找到了!
}
進行>= 0檢查是因為indexOf(..)在找到結果時返回一個0或更大的數(shù)字值,或者在沒找到結果時返回-1。換句話說,我們在一個布爾值的上下文環(huán)境中使用了一個返回索引的函數(shù)。而由于-1是truthy而非falsy,所以我們不得不手動進行檢查。
在本系列的 類型與文法 中,我探索了另一種我稍稍偏好的模式:
var vals = [ "foo", "bar", 42, "baz" ];
if (~vals.indexOf( 42 )) {
// 找到了!
}
這里的~操作符使indexOf(..)的返回值與一個值的范圍相一致,這個范圍可以恰當?shù)貜娭妻D換為布爾型。也就是,-1產生0(falsy),而其余的東西產生非零值(truthy),而這正是我們判定是否找到值的依據(jù)。
雖然我覺得這是一種改進,但有另一些人強烈反對。然而,沒有人會質疑indexOf(..)的檢索邏輯是完美的。例如,在數(shù)組中查找NaN值會失敗。
于是一個提案浮出了水面并得到了大量的支持 —— 增加一個真正的返回布爾值的數(shù)組檢索方法,稱為includes(..):
var vals = [ "foo", "bar", 42, "baz" ];
if (vals.includes( 42 )) {
// 找到了!
}
注意: Array#includes(..)使用了將會找到NaN值的匹配邏輯,但將不會區(qū)分-0與0(參見本系列的 類型與文法)。如果你在自己的程序中不關心-0值,那么它很可能正是你希望的。如果你 確實 關心-0,那么你就需要實現(xiàn)你自己的檢索邏輯,很可能是使用Object.is(..)工具(見六章)。
SIMD
我們在本系列的 異步與性能 中詳細講解了一個指令,多個數(shù)據(jù)(SIMD),但因為它是未來JS中下一個很可能被確定下來的特性,所以這里簡要地提一下。
SIMD API 暴露了各種底層(CPU)指令,它們可以同時操作一個以上的數(shù)字值。例如,你可以指定兩個擁有4個或8個數(shù)字的 向量,然后一次性分別相乘所有元素(數(shù)據(jù)并行機制!)。
考慮如下代碼:
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()、以及其他許多。
并行數(shù)學操作對下一代的高性能JS應用程序至關重要。
WebAssembly (WASM)
在本書的第一版將近完成的時候,Brendan Eich 突然宣布了一個有可能對JavaScript未來的道路產生重大沖擊的公告:WebAssembly(WASM)。我們不能在這里詳細地探討WASM,因為在本書寫作時這個話題為時過早了。但如果不簡要地提上一句,這本書就不夠完整。
JS語言在近期(和近未來的)設計的改變上所承受的最大壓力之一,就是渴望它能夠成為從其他語言(比如 C/C++,ClojureScript,等等)轉譯/交叉編譯來的、合適的目標語言。顯然,作為JavaScript運行的代碼性能是一個主要問題。
正如在本系列的 異步與性能 中討論過的,幾年前一組在Mozilla的開發(fā)者給JavaScript引入了一個稱為ASM.js的想法。AMS.js是一個合法JS的子集,它大幅地制約了使代碼難于被JS引擎優(yōu)化的特定行為。其結果就是兼容AMS.js的代碼在一個支持ASM的引擎上可以顯著地快速運行,幾乎可以與優(yōu)化過的原生C語言的等價物相媲美。許多觀點認為,對于那些將要由JavaScript編寫的渴求性能的應用程序來說,ASM.js很可能將是它們的基干。
換言之,在瀏覽器中條條大路通過JavaScript通向運行的代碼。
直到WASM公告之前,是這樣的。WASM提供了另一條路線,讓其他語言不必非得首先通過JavaScript就能將瀏覽器的運行時環(huán)境作為運行的目標。實質上,如果WASM啟用,JS引擎將會生長出額外的能力 —— 執(zhí)行可以被視為有些與字節(jié)碼相似的二進制代碼(就像在JVM上運行的那些東西)。
WASM提出了一種高度壓縮的代碼AST(語法樹)的二進制表示格式,它可以繼而像JS引擎以及它的基礎結構直接發(fā)出指令,無需被JS解析,甚至無需按照JS的規(guī)則動作。像C或C++這樣的語言可以直接被編譯為WASM格式而非ASM.js,并且由于跳過JS解析而得到額外的速度優(yōu)勢。
短期內,WASM與AMS.js、JS不相上下。但是最終,人們預期WASM將會生長出新的能力,那將超過JS能做的任何事情。例如,讓JS演化出像線程這樣的根本特性 —— 一個肯定會對JS生態(tài)系統(tǒng)造成重大沖擊的改變 —— 作為一個WASM未來的擴展更有希望,也會緩解改變JS的壓力。
事實上,這張新的路線圖為許多語言服務于web運行時開啟了新的道路。對于web平臺來說,這真是一個激動人心的新路線!
它對JS意味著什么?JS將會變得無關緊要或者“死去”嗎?絕對不是。ASM.js在接下來的幾年中很可能看不到太多未來,但JS在數(shù)量上的絕對優(yōu)勢將它安全地錨定在web平臺中。
WASM的擁護者們說,它的成功意味著JS的設計將會被保護起來,遠離那些最終會迫使它超過自己合理性的臨界點的壓力。人們估計WASM將會成為應用程序中高性能部分的首選目標語言,這些部分曾用各種各樣不同的語言編寫過。
有趣的是,JavaScript是未來不太可能以WASM為目標的語言之一??赡苡幸恍┪磥淼母淖儠谐鯦S的一部分,而使這一部分更適于以WASM作為目標,但是這件事情看起來優(yōu)先級不高。
雖然JS很可能與WASM沒什么關聯(lián),但JS代碼和WASM代碼將能夠以最重要的方式進行交互,就像當下的模塊互動一樣自然。你可以想象,調用一個foo()之類的JS函數(shù)而使它實際上調用一個同名WASM函數(shù),它具備遠離你其余JS的制約而運行的能力。
至少是在可預見的未來,當下以JS編寫的東西可能將繼續(xù)總是由JS編寫。轉譯為JS的東西將可能最終至少考慮以WASM為目標。對于那些需要極致性能,而且在抽象的層面上沒有余地的東西,最有可能的選擇是找一種合適的非JS語言編寫,然后以WASM為目標語言。
這個轉變很有可能將會很慢,會花上許多年成形。WASM在所有的主流瀏覽器上固定下來可能最快也要花幾年。同時,WASM項目(https://github.com/WebAssembly)有一個早期填補,來為它的基本原則展示概念證明。
但隨著時間的推移,也隨著WASM學到新的非JS技巧,不難想象一些當前是JS的東西被重構為以WASM作為目標的語言。例如,框架中性能敏感的部分,游戲引擎,和其他被深度使用的工具都很可能從這樣的轉變中獲益。在web應用程序中使用這些工具的開發(fā)者們并不會在使用或整合上注意到太多不同,但確實會自動地利用這些性能和能力。
可以確定的是,隨著WASM變得越來越真實,它對JavaScript設計路線的影響就越來越多。這可能是開發(fā)者們應當關注的最重要的“ES6以后”的話題。
復習
如果這個系列的其他書目實質上提出了這個挑戰(zhàn),“你(可能)不懂JS(不像自己想象的那么懂)”,那么這本書就是在說,“你不再懂JS了”。這本書講解了在ES6中加入到語言里的一大堆新東西。它是一個新語言特性的精彩集合,也是將永遠改進我們JS程序的范例。
但JS不是到ES6就完了!還早得很呢。已經(jīng)有好幾個“ES6之后”的特性處于開發(fā)的各個階段。在這一章中,我們簡要地看了一些最有可能很快會被固定在JS中的候選特性。
async function是建立在 generators + promises 模式(見第四章)上的強大語法糖。Object.observe(..)為監(jiān)聽對象變化事件增加了直接原生的支持,它對實現(xiàn)數(shù)據(jù)綁定至關重要。**指數(shù)作符,針對對象屬性的...,以及Array#includes(..)都是對現(xiàn)存機制的簡單而有用的改進。最后,SIMD將高性能JS的演化帶入一個新紀元。
聽起來很俗套,但JS的未來是非常光明的!這個系列,以及這本書的挑戰(zhàn),現(xiàn)在是各位讀者的職責了。你還在等什么?是時候開始學習和探索了!