原文:TC39, ECMAScript, and the Future of JavaScript
作者:Nicolás Bevacqua
譯者序
很榮幸能夠和 Nicolás Bevacqua 同臺分享。Nicolás Bevacqua 分享了《the Future of Writing JavaScript 》,我在其後分享了《面向前端開發者的V8效能優化》。如果想了解更多 V8 知識可以關注我的專欄:V8 引擎。
由於 Nicolás Bevacqua 是英文分享,現場由很多聽眾都沒有太明白,會後我聯絡了 Nicolás Bevacqua 爭得大神同意後將其文章翻譯為中文。
大神微信玩的很溜,很快就學會了搶紅包。
再次感謝 Nicolás Bevacqua 的精彩分享。
譯文:
上週,我在中國深圳的騰訊前端大會上發表了與本文同名的演講。在這篇文章中,我根據 PonyFoo 網站的格式重新編輯了一遍。我希望你喜歡它!
TC39 是什麼?
TC39 指的是技術委員會(Technical Committee)第 39 號。它是 ECMA 的一部分,ECMA 是 “ECMAScript” 規範下的 JavaScript 語言標準化的機構。
ECMAScript 規範定義了 JavaScript 如何一步一步的進化、發展。其中規定了:
- 字串
'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複製程式碼
它還解釋了正零與負零什麼情況下相等,什麼情況下不相等。。。
+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。一個提案越成熟,越有可能最終將其納入規範。
Stage 0
任何尚未提交作為正式提案的討論、想法變更或者補充都被認為是第 0 階段的“稻草人”提案。只有 TC39 的成員可以建立這些提案,而且今天就有若干活躍的“稻草人”提案。
目前在 Stage 0 的提案包括非同步操作的 cancellation tokens , Zones 作為 Angular 團隊的一員,提供了很多建議。Stage 0 包括了很多一直沒有進入 Stage 1 的提案。
在這篇文章的後面,我們將仔細分析一部分提案。
Stage 1
在 Stage 1,提案已經被正式化,並期望解決此問題,還需要觀察與其他提案的相互影響。在這個階段的提案確定了一個分散的問題,併為這個問題提供了具體的解決方案。
Stage 1 提議通常包括高階 API 描述(high level AP),使用示例以及內部語義和演算法的討論。這些建議在通過這一過程時可能會發生重大變化。
Stage 1 目前提案的例子包括:Observable、do 表示式、生成器箭頭函式、Promise.try。
Stage 2
Stage 2 的提案應提供規範初稿。
此時,語言的實現者開始觀察 runtime 的具體實現是否合理。該實現可以使用 polyfill 的方式,以便使程式碼可在 runtime 中的行為負責規範的定義; javascript 引擎的實現為提案提供了原生支援; 或者可以 Babel 這樣的編譯時編譯器來支援。
目前 Stage 2 階段的提案有 public class fields、private class fields、decorators、Promise#finally、等等。
Stage 3
Stage 3 提案是建議的候選提案。在這個高階階段,規範的編輯人員和評審人員必須在最終規範上簽字。Stage 3 的提案不會有太大的改變,在對外發布之前只是修正一些問題。
語言的實現者也應該對此提案感興趣 - 如果只是提案卻沒有具體實現去支援這個提案,那麼這個提案早就胎死腹中了。事實上,提案至少具有一個瀏覽器實現、友好的 polyfill或者由像 Babel 這樣的構建時編譯器支援。
Stage 3 由很多令人興奮的功能,如物件的解析與剩餘,非同步迭代器,import() 方法和更好的 Unicode 正規表示式支援。
Stage 4
最後,當規範的實現至少通過兩個驗收測試時,提案進入 Stage 4。
進入 Stage 4 的提案將包含在 ECMAScript 的下一個修訂版中。
非同步函式,Array#includes 和 冪運算子 是 Stage 4 的一些特性。
保持最新 Staying Up To Date
我(原文作者)建立了一個網站,用來展示當前提案的列表。它描述了他們在什麼階段,並連結到每個提案,以便您可以更多地瞭解它們。
網址為 proptt39.now.sh。
目前,每年都有新的正式規範版本,但精簡的流程也意味著正式版本變得越來越不相關。現在重點放在提案階段,我們可以預測,在 ES6 之後,對該標準的具體修訂的引用將變得不常見。
提案 Proposals
我們來看一些目前正在開發的最有趣的提案。
Array#includes (Stage 4)
在介紹 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複製程式碼
非同步函式(Stage 4)
當我們使用 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)
非同步迭代器已經進入了 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
}
}複製程式碼
物件解構與剩餘(Stage 3)
從 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' }複製程式碼
動態 import()(Stage 3)
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))複製程式碼
未完。。。。