原文:TC39, ECMAScript, and the Future of JavaScript
做者:Nicolás Bevacquajavascript
很榮幸可以和 Nicolás Bevacqua 同臺分享。Nicolás Bevacqua 分享了《the Future of Writing JavaScript 》,我在其後分享了《面向前端開發者的V8性能優化》。若是想了解更多 V8 知識能夠關注個人專欄:V8 引擎。前端
因爲 Nicolás Bevacqua 是英文分享,現場由不少聽衆都沒有太明白,會後我聯繫了 Nicolás Bevacqua 爭得大神贊成後將其文章翻譯爲中文。java
大神微信玩的很溜,很快就學會了搶紅包。git
再次感謝 Nicolás Bevacqua 的精彩分享。es6
譯文:github
上週,我在中國深圳的騰訊前端大會上發表了與本文同名的演講。在這篇文章中,我根據 PonyFoo 網站的格式從新編輯了一遍。我但願你喜歡它!正則表達式
TC39 指的是技術委員會(Technical Committee)第 39 號。它是 ECMA 的一部分,ECMA 是 「ECMAScript」 規範下的 JavaScript 語言標準化的機構。算法
ECMAScript 規範定義了 JavaScript 如何一步一步的進化、發展。其中規定了:express
'A'
爲何是 NaN
'A'
爲何不等於 NaN
NaN
爲何是 NaN
,但卻不等於 NaN
Number.isNaN
是一個很好的 idea ...isNaN(NaN) // true
isNaN('A') // true
'A' == NaN // false
'A' === NaN // false
NaN === NaN // false
// … 解決方案!
Number.isNaN('A') // false
Number.isNaN(NaN) // true複製代碼
它還解釋了正零與負零什麼狀況下相等,什麼狀況下不相等。。。json
+0 == -0 // true
+0 === -0 // true
1/+0 === 1 / -0 // false複製代碼
並且 js 中還有不少奇技淫巧,例如只使用感嘆號、小括號、方括號和加號來編碼任何有效的 JavaScript 表達式。能夠在 JSFuck 網站了解更多關於如何只使用 +!()[]
編寫 JavaScript 代碼的技巧。
不論如何,TC39 所作的不懈努力是難能難得的。
TC39 遵循的原則是:分階段加入不一樣的語言特性。一旦提案成熟,TC39 會根據提案中的變更來更新規範。直到最近,TC39 依然依賴基於 Microsoft Word 的比較傳統的工做流程。但 ES3 出來以後,他們花了十年時間,幾乎沒有任何改變,使其達到規範。以後,ES6 又花了四年才能實現。
顯然,他們的流程必須改善。
自 ES6 出來以後,他們精簡了提案的修訂過程,以知足現代化開發的需求。新流程使用 HTML 的超集來格式化提案。他們使用 GitHub pull requests,這有助於增長社區的參與,而且提出的提案數量也增長了。這個規範如今是一個 living standard,這意味着提案會更快,並且咱們也不用等待新版本的規範出來。
新流程涉及四個不一樣的 Stage。一個提案越成熟,越有可能最終將其歸入規範。
任何還沒有提交做爲正式提案的討論、想法變動或者補充都被認爲是第 0 階段的「稻草人」提案。只有 TC39 的成員能夠建立這些提案,並且今天就有若干活躍的「稻草人」提案。
目前在 Stage 0 的提案包括異步操做的 cancellation tokens , Zones 做爲 Angular 團隊的一員,提供了不少建議。Stage 0 包括了不少一直沒有進入 Stage 1 的提案。
在這篇文章的後面,咱們將仔細分析一部分提案。
在 Stage 1,提案已經被正式化,並指望解決此問題,還須要觀察與其餘提案的相互影響。在這個階段的提案肯定了一個分散的問題,併爲這個問題提供了具體的解決方案。
Stage 1 提議一般包括高階 API 描述(high level AP),使用示例以及內部語義和算法的討論。這些建議在經過這一過程時可能會發生重大變化。
Stage 1 目前提案的例子包括:Observable、do 表達式、生成器箭頭函數、Promise.try。
Stage 2 的提案應提供規範初稿。
此時,語言的實現者開始觀察 runtime 的具體實現是否合理。該實現可使用 polyfill 的方式,以便使代碼可在 runtime 中的行爲負責規範的定義; javascript 引擎的實現爲提案提供了原生支持; 或者能夠 Babel 這樣的編譯時編譯器來支持。
目前 Stage 2 階段的提案有 public class fields、private class fields、decorators、Promise#finally、等等。
Stage 3 提案是建議的候選提案。在這個高級階段,規範的編輯人員和評審人員必須在最終規範上簽字。Stage 3 的提案不會有太大的改變,在對外發布以前只是修正一些問題。
語言的實現者也應該對此提案感興趣 - 若是隻是提案卻沒有具體實現去支持這個提案,那麼這個提案早就胎死腹中了。事實上,提案至少具備一個瀏覽器實現、友好的 polyfill或者由像 Babel 這樣的構建時編譯器支持。
Stage 3 由不少使人興奮的功能,如對象的解析與剩餘,異步迭代器,import() 方法和更好的 Unicode 正則表達式支持。
最後,當規範的實現至少經過兩個驗收測試時,提案進入 Stage 4。
進入 Stage 4 的提案將包含在 ECMAScript 的下一個修訂版中。
異步函數,Array#includes 和 冪運算符 是 Stage 4 的一些特性。
我(原文做者)建立了一個網站,用來展現當前提案的列表。它描述了他們在什麼階段,並連接到每一個提案,以便您能夠更多地瞭解它們。
網址爲 proptt39.now.sh。
目前,每一年都有新的正式規範版本,但精簡的流程也意味着正式版本變得愈來愈不相關。如今重點放在提案階段,咱們能夠預測,在 ES6 以後,對該標準的具體修訂的引用將變得不常見。
咱們來看一些目前正在開發的最有趣的提案。
在介紹 Array#includes
以前,咱們不得不依賴 Array#indexOf
函數,並檢查索引是否超出範圍,以肯定元素是否屬於數組。
隨着 Array#includes
進入 Stage 4,咱們可使用 Array#includes
來代替。它補充了 ES6 的 Array#find
和 Array#findIndex
。
[1, 2].indexOf(2) !== -1 // true
[1, 2].indexOf(3) !== -1 // false
[1, 2].includes(2) // true
[1, 2].includes(3) // false複製代碼
當咱們使用 Promise 時,咱們常常考慮執行線程。咱們有一個異步任務 fetch
,其餘任務依賴於 fetch
的響應,但在收到該數據以前程序時阻塞的。
在下面的例子中,咱們從 API 中獲取產品列表,該列表返回一個 Promise
。當 fetch 相應以後,Promise 被 resolve。而後,咱們將響應流做爲 JSON 讀取,並使用響應中的數據更新視圖。若是在此過程當中發生任何錯誤,咱們能夠將其記錄到控制檯,以瞭解發生了什麼。
fetch('/api/products')
.then(response => response.json())
.then(data => {
updateView(data)
})
.catch(err => {
console.log('Update failed', err)
})複製代碼
異步函數提供了語法糖,能夠用來改進咱們基於 Promise
的代碼。咱們開始逐行改變以上基於 Promise 的代碼。咱們可使用 await
關鍵字。當咱們 await
一個 Promise 時,咱們獲得 Promise 的 fulled 狀態的值。
Promise 代碼的意思是:「我想執行這個操做,而後(then)在其餘操做中使用它的結果」。
同時,await
有效地反轉了這個意思,使得它更像:「我想要取得這個操做的結果」。我喜歡,由於它聽起來更簡單。
在咱們的示例中,響應對象是咱們以後獲取的,因此咱們將等待(await
)獲取(fetch
)操做的結果,並賦值給 response
變量,而不是使用 promise
的 then
。
原文:we’ll flip things over and assigned the result of await
fetch
to the response
variable
+ const response = await fetch('/api/products')
- fetch('/api/products')
.then(response => response.json())
.then(data => {
updateView(data)
})
.catch(err => {
console.log('Update failed', err)
})複製代碼
咱們給 response.json()
一樣的待遇。咱們 await
上一次的操做並將其賦值給 data
變量。
const response = await fetch('/api/products')
+ const data = await response.json()
- .then(response => response.json())
.then(data => {
updateView(data)
})
.catch(err => {
console.log('Update failed', err)
})複製代碼
既然 then
鏈已經消失了,咱們就能夠直接調用 updateView
語句了,由於咱們已經到了以前代碼中的 Promise then 鏈的盡頭,咱們不須要等待任何其餘的 Promise。
const response = await fetch('/api/products')
const data = await response.json()
+ updateView(data)
- .then(data => {
- updateView(data)
- })
.catch(err => {
console.log('Update failed', err)
})複製代碼
如今咱們可使用 try/catch
塊,而不是 .catch
,這使得咱們的代碼更加語義化。
+ try {
const response = await fetch('/api/products')
const data = await response.json()
updateView(data)
+ } catch(err) {
- .catch(err => {
console.log('Update failed', err)
+ }
- )}複製代碼
一個限制是 await
只能在異步函數內使用。
+ async function run() {
try {
const response = await fetch('/api/products')
const data = await response.json()
updateView(data)
} catch(err) {
console.log('Update failed', err)
}
+ }複製代碼
可是,咱們能夠將異步函數轉換爲自調用函數表達式。若是咱們將頂級代碼包在這樣的表達式中,咱們能夠在代碼中的任何地方使用 await
表達式。
一些社區但願原生支持頂級塊做用於的 await
,而另一些人則認爲這會對用戶形成負面影響,由於一些庫可能會阻塞異步加載,從而大大減緩了咱們應用程序的加載時間。
+ (async () => {
- async function run() {
try {
const response = await fetch('/api/products')
const data = await response.json()
updateView(data)
} catch(err) {
console.log('Update failed', err)
}
+ })()
- }複製代碼
就我的而言,我認爲在 JavaScript 性能中已經有足夠的空間來應對這種愚蠢的事情,來優化初始化的庫使用 await
的行爲。
請注意,您也能夠在 non-promise 的值前面使用 await
,甚至編寫代碼 await (2 + 3)
。在這種狀況下,(2 + 3)
表達的結果會被包在 Promise 中,做爲 Promise 的最終值。5
成爲這個 await
表達式的結果。
請注意,await
加上任何 JavaScript 表達式也是一個表達式。這意味着咱們不限制 await
語句的賦值操做,並且咱們也能夠把 await
函數調用做爲模板文字插值的一部分。
`Price: ${ await getPrice() }`複製代碼
或做爲另外一個函數調用的一部分...
renderView(await getPrice())複製代碼
甚至做爲數學表達式的一部分。
2 * (await getPrice())複製代碼
最後,無論它們的內容如何,異步函數老是返回一個 Promise。這意味着咱們能夠添加 .then
或 .catch
等異步功能,也可使用 await
獲取最終的結果。
const sleep = delay => new Promise(resolve =>
setTimeout(resolve, delay)
)
const slowLog = async (...terms) => {
await sleep(2000)
console.log(...terms)
}
slowLog('Well that was underwhelming')
.then(() => console.log('Nailed it!'))
.catch(reason => console.error('Failed', reason))複製代碼
正如您所指望的那樣,返回的 Promise 與 async
函數返回的值進行運算,或者被 catch 函數來處理任何未捕獲的異常。
異步迭代器已經進入了 Stage 3。在瞭解異步迭代器以前,讓咱們簡單介紹一下 ES6 中引入的迭代。迭代能夠是任何遵循迭代器協議的對象。
爲了使對象能夠迭代,咱們定義一個 Symbol.iterator
方法。迭代器方法應該返回一個具備 next
方法的對象。這個對象描述了咱們的 iterable
的順序。當對象被迭代時,每當咱們須要讀取序列中的下一個元素時,將調用 next
方法。value
用來獲取序列中每個對象的值。當返回的對象被標記爲 done
,序列結束。
const list = {
[Symbol.iterator]() {
let i = 0
return {
next: () => ({
value: i++,
done: i > 5
})
}
}
}
[...list]
// <- [0, 1, 2, 3, 4]
Array.from(list)
// <- [0, 1, 2, 3, 4]
for (const i of list) {
// <- 0, 1, 2, 3, 4
}複製代碼
可使用 Array.from
或使用擴展操做符使用 Iterables
。它們也能夠經過使用 for..of
循環來遍歷元素序列。
異步迭代器只有一點點不一樣。在這個提議下,一個對象經過 Symbol.asyncIterator
來表示它們是異步迭代的。異步迭代器的方法簽名與常規迭代器的約定略有不一樣:該 next
方法須要返回 包裝了 { value, done }
的 Promise
,而不是 { value, done }
直接返回。
const list = {
[Symbol.asyncIterator]() {
let i = 0
return {
next: () => Promise.resolve({
value: i++,
done: i > 5
})
}
}
}複製代碼
這種簡單的變化很是優雅,由於 Promise 能夠很容易地表明序列的最終元素。
異步迭代不能與數組擴展運算符、Array.from
、for..of
一塊兒使用,由於這三個都專門用於同步迭代。
這個提案也引入了一個新的 for await..of
結構。它能夠用於在異步迭代序列上語義地迭代。
for await (const i of items) {
// <- 0, 1, 2, 3, 4
}複製代碼
請注意,該 for await..of
結構只能在異步函數中使用。不然咱們會獲得語法錯誤。就像任何其餘異步函數同樣,咱們也能夠在咱們的循環周圍或內部使用 try/catch
塊 for await..of
。
async function readItems() {
for await (const i of items) {
// <- 0, 1, 2, 3, 4
}
}複製代碼
更進一步。還有異步生成器函數。與普通生成器函數有些類似,異步生成器函數不只支持 async
await
語義,還容許 await
語句以及 for await..of
。
(原文第一段:The rabbit hole goes deeper of course. 這是愛麗絲夢遊仙境的梗嗎?)
async function* getProducts(categoryUrl) {
const listReq = await fetch(categoryUrl)
const list = await listReq.json()
for (const product of list) {
const productReq = await product.url
const product = await productReq.json()
yield product
}
}複製代碼
在異步生成器函數中,咱們可使用 yield*
與其餘異步發生器和普通的發生器一塊兒使用。當調用時,異步生成器函數返回異步生成器對象,其方法返回包裹了 { value, done }
的 Promise,而不是 { value, done }
。
最後,異步生成器對象能夠被使用在 for await..of
,就像異步迭代同樣。這是由於異步生成器對象是異步迭代,就像普通生成器對象是普通的迭代。
async function readProducts() {
const g = getProducts(category)
for await (const product of g) {
// use product details
}
}複製代碼
從 ES6 開始,咱們使用 Object.assign
將屬性從一個或多個源對象複製到一個目標對象上。在下一個例子中,咱們將一些屬性複製到一個空的對象上。
Object.assign(
{},
{ a: 'a' },
{ b: 'b' },
{ a: 'c' }
)複製代碼
對象解構(spread)提議容許咱們使用純語法編寫等效的代碼。咱們從一個空對象開始,Object.assign
隱含在語法中。
{
...{ a: 'a' },
...{ b: 'b' },
...{ a: 'c' }
}
// <- { a: 'c', b: 'b' }複製代碼
和對象解構相反的還有對象剩餘,相似數組的剩餘參數。當對對象進行解構時,咱們可使用對象擴展運算符將模式中未明確命名的屬性重建爲另外一個對象。
在如下示例中,id 顯式命名,不會包含在剩餘對象中。對象剩餘(rest)能夠從字面上讀取爲「全部其餘屬性都轉到一個名爲 rest 的對象」,固然,變量名稱供您選擇。
const item = {
id: '4fe09c27',
name: 'Banana',
amount: 3
}
const { id, ...rest } = item
// <- { name: 'Banana', amount: 3 }複製代碼
在函數參數列表中解析對象時,咱們也可使用對象剩餘屬性。
function print({ id, ...rest }) {
console.log(rest)
}
print({ id: '4fe09c27', name: 'Banana' })
// <- { name: 'Banana' }複製代碼
ES6 引入了原生 JavaScript 模塊。與 CommonJS 相似,JavaScript 模塊選擇了靜態語法。這樣開發工具備更簡單的方式從靜態源碼中分析和構建依賴樹,這使它成爲一個很好的默認選項。
import markdown from './markdown'
// …
export default compile複製代碼
然而,做爲開發人員,咱們並不老是知道咱們須要提早導入的模塊。對於這些狀況,例如,當咱們依賴本地化來加載具備用戶語言的字符串的模塊時,Stage 3 的動態 import()
提案就頗有用了。
import()
運行時動態加載模塊。它爲模塊的命名空間對象返回 Promise,當獲取該對象時,系統將解析和執行所請求的模塊及其全部依賴項。若是模塊加載失敗,Promise 將被拒絕。
import(`./i18n.${ navigator.language }.js`)
.then(module => console.log(module.messages))
.catch(reason => console.error(reason))複製代碼
未完。。。。