[譯] 那些好玩卻尚未被 ECMAScript 2017 採納的提案

Colafornia發表於2018-05-02

[譯] 那些好玩卻尚未被 ECMAScript 2017 採納的提案

要跟上所有新功能提案的進度並不容易。每年,管理 JavaScript 發展的委員會 TC39 都會收到數十個提案。由於大多數提案都不會到達第二階段,因此很難確定哪些提案值得關注,哪些提案只是奇思妙想(或者稱之為異想天開)。

由於現在越來越多的提案湧現出來,想要只停留在這些特性提案的頂層會非常困難。在過去介於 ES5 和 ES6 之間的六年時間裡 JavaScript 的發展腳步非常保守。自 ECMAScript 2016 (ES7) 釋出,釋出過程要求為每年釋出一次,並且更加標準化

隨著近些年來 polyfills 和轉譯器的流行,一些尚屬早期(early-stage)的提案甚至在還未最終確定前就已經被廣泛使用了。並且,由於提案在被採納之前會有很大變動,一些開發者可能會發現他們所使用的特性永遠不會變成 JavaScript 的語言實現。

在深入研究那些我覺得很好玩的提案之前,我們先花點時間熟悉一下目前的提案流程。

ECMAScript 提案流程中的 5 個 stage

Stage 0 “稻草人” —— 這是所有提案的起點。在進入下一階段之前,提案的內容可能會發生重大變化。目前還沒有提案的接收標準,任何人都可以為這一階段提交新的提案。無需任何程式碼實現,規範也無需合乎標準。這個階段的目的是開始針對該功能特性的討論。目前已經有超過 20 個處於 stage 0 的提案

Stage 1 “提案” —— 一個真正的正式提案。此階段的提案需要一個“擁護者”(即 TC39 委員會的成員)。此階段需仔細考慮 API 並描述出任何潛在的、程式碼實現方面的挑戰。此階段也需開發 polyfill 併產出 demo。在這一階段之後提案可能會發生重大變化,因此需小心使用。目前仍處於這一階段的提案包括了已望穿秋水的 Observables typePromise.try 功能。

Stage 2 “草案” —— 此階段將使用正式的 TC39 規範語言來精確描述語法。在此階段後仍由可能發生一些小修改,但是規範應該足夠完整,無需進行重大修訂。如果一個提案走到了這一步,那麼很有可能委員會是希望最終可以實現該功能的。

Stage 3 “候選” —— 該提案已獲批准,僅當執行作者提出要求時才會做進一步的修改。此時你可以期待 JavaScript 引擎中開始實現提案的功能了。在這一階段草案的 polyfill 可以安全無憂使用。

Stage 4 “完成” —— 說明提案已被採納,提案規範將與 JavaScript 規範合併。預計不會再發生變化。JavaScript 引擎將釋出它們的實現。截至 2017 年 10 月,已經有 9 個已完成的提案,其中最引人關注的是 async functions

由於提案越來越多,思考一番,以下幾個提案是其中更有趣的。

[譯] 那些好玩卻尚未被 ECMAScript 2017 採納的提案

圖源 xkcd.com/927/

Asynchronous Iteration 非同步迭代

ECMAScript 2015 中引入了迭代器 iterator,其中包含了 for-of 迴圈語法。這使得迴圈遍歷可迭代物件變得相當容易,並且可以實現你自己的可迭代資料結構。

遺憾的是,遍歷器無法用於表示非同步的資料結構如訪問檔案系統。雖然你可以執行 Promise.all 來遍歷一系列的 promise,但這需要同步確定“已完成”的狀態。

例如,可以使用非同步迭代器來遍歷非同步內容,按需讀取檔案中內容,而不是提前讀取檔案中的所有內容。

你可以通過簡單地同時使用 generator 生成器語法和 async-await 語法來定義非同步生成器函式:

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}
複製程式碼

非同步生成器函式示例。

示例

可以在 for-await-of 迴圈中使用這個非同步生成器:

for await (const line of readLines(filePath)) {
  console.log(line);
}
複製程式碼

使用 for-await-of。

任意具有 Symbol.asyncIterator 屬性的物件都被定義為 async iterable,並且可使用於新的 for-await-of 語法中。這有一個具體可執行的示例:

class LineReader() {
 constructor(filepath) {
   this.filepath = filepath;
   this.file = fileOpen(filepath);
 }

 [Symbol.asyncIterator]: {
   next() {
     return new Promise((resolve, reject) => {
       if (this.file.EOF) {
         resolve({ value: null, done: true });
       } else {
         this.file.readLine()
           .then(value => resolve({ value, done: false }))
           .catch(error => reject(error));
       }
     });
   }
 }
}
複製程式碼

使用 Symbol.asyncIterator 的示例。

這一提案 目前處於 stage 3,瀏覽器已經開始實現了。處於這一階段意味著它很有可能會被合併入標準並可以在主流瀏覽器中使用。但是,在此之前,規範可能會有一些小修改,因此現在使用非同步迭代器會帶來一定程度的風險。

regenerator 專案目前已為非同步迭代器提案提供了基本支援。但是,它本身並不支援 for-await-of 迴圈語法。Babel 外掛 transform-async-generator-functions 既支援非同步生成器又支援 for-await-of 迴圈語法。

Class 優化

這個提案 建議向 [ECMAScript 2015 中引進的 class 語法] (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) 中加入公共欄位、私有欄位與私有方法。該提案是經過多個競爭提案漫長討論和競爭後的結果。

使用私有欄位和方法與對應的公共欄位和方法類似,但是私有欄位名方法名前會有一個 # 號。任何被標記為私有的方法和欄位都不會在類之外可見,從而確保內部類成員的強封裝。

下面是一個類 React 元件的假設示例,元件在私有方法中使用了公共和私有欄位:

class Counter {
  // 公共欄位
  text = ‘Counter’;

  // 私有欄位
  #state = {
    count: 0,
  };

  // 私有方法
  #handleClick() {
    this.#state.count++;
  }

  // 公共方法
  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        {this.text}: {this.#state.count.toString()}
      </button>
    );
  }
}
複製程式碼

使用了私有欄位與私有方法的類 React 元件。

Babel 目前還沒有提供私有類欄位與方法的 polyfill,但是不久就會實現。公共欄位已有 Babel 的transform-class-properties 外掛支援,但它依賴於一個已被合併入統一公共/私有欄位提案的老提案。此提案於 2017 年 9 月 1 日 進入 stage 3,因此使用任何可用的 polyfill 都是安全的。

Class 裝飾器

提案在被引入後也可能發生翻天覆地的變化,裝飾器就是一個很好的例子。Babel 的第五代版本實現了原本 stage 2 階段裝飾器的規範,其將裝飾器定義為接收 target,name 與屬性描述的函式。現在最流行的轉譯裝飾器方式是通過 Babel 的transform-legacy-decorators 外掛,其實現的是舊版的規範。

新的提案 大不相同。不再作為具有三個屬性的函式,現在我們對改變描述符的類成員 —— 裝飾器進行了正式描述。新的“成員描述符”與ES5中引入的屬性描述符介面非常相似。

現在有兩種具有不同 API 的不同型別的裝飾器:成員裝飾器與類裝飾器。

  • 成員裝飾器接收成員描述符並返回成員描述符。
  • 類裝飾器接受每個類成員(即每個屬性或方法)的建構函式、父類和成員描述符陣列。

在規範中,“額外”是指可由裝飾器新增的成員描述符的可選陣列。這將允許裝飾器動態建立新的屬性與方法。比如,你可以讓裝飾器給屬性建立 getter 與 setter 函式。

與舊規範類似,新規範允許修改類成員的描述符。此外,仍然允許在物件字面量的屬性上使用裝飾器。

在最終確定之前,規範很可能會發生重大變化。語法中有一些模稜兩可之處,舊規範的許多痛點還沒有得到解決。裝飾器是語言的一個大型語法擴充套件,因此可以預料到這種延遲。遺憾的是,如果新的提案被採納,你將不得不完全重構你的裝飾器函式,以適用於新的介面。

許多庫作者選擇繼續支援舊的提案和 Babel 的 legacy-decorators 外掛,即使新的提案已經處於 stage 2,舊的仍然處於 stage 0。core-decorators 作為最受歡迎的使用裝飾器的 JavaScript 開源庫,就採用了這種方法。未來幾年中,庫的作者們很有可能會繼續支援舊的提案。

也有可能這一新提案會被撤回,取而代之的是另一新提案,裝飾器提案有可能不會在 2018 年併入 JavaScript。你可以在 Babel 完成新的轉譯外掛 後使用新的裝飾器提案。

import 函式

ECMAScript 第六版中新增了 import 語句,並最終確定了新的模組系統的語義。就在近期,主流瀏覽器釋出更新以提供對其的支援,儘管它們對於規範的實現略有不同。NodeJS 在 8.5.0 版本中對於仍帶有實驗標誌的 ECMAScript 的模組規範提供了初步支援。

但是,該提案缺少一種非同步匯入模組的方法,這使得難以在執行時動態匯入模組。現在,在瀏覽器中動態載入模組的唯一方法,就是動態插入型別為 “module” 的 script 標籤,將 import 宣告作為其文字內容。

實現非同步匯入模組的一種內建方法是提案 動態 import 語法,它會呼叫一個“類函式”的匯入模組載入表單。這種動態匯入語法可以在模組程式碼與普通指令碼程式碼中使用,從而為模組程式碼提供了一個方便的切入點。

去年有一個提案提出 System.import() 函式來解決這個問題,但是該提案沒有被採納進入最終的規範。新提案目前處於 stage 3,有望在年底前被列入規範中。

Observables

提議的可觀察型別 Observable type 提供了一種處理非同步資料流的標準化方法。它們已經以某種形式在許多流行的 JavaScript 框架如 RxJS 中實現。目前的提案很大程度上借鑑了這些框架的思路。

Observable 物件由 Observable 構造器建立,接收訂閱函式作為引數:

function listen(element, eventName) {
 return new Observable(observer => {
   // 建立一個事件處理函式,可以將資料輸出
   let handler = event => observer.next(event);

   // 繫結事件處理函式
   element.addEventListener(eventName, handler, true);

   // 返回一個函式,呼叫它即去掉訂閱
   return () => {
     // 解除元素的事件監聽
     element.removeEventListener(eventName, handler, true);
   };
 });
}
複製程式碼

Observable 構造器的使用。

使用訂閱函式去訂閱一個 observable 物件:

const subscription = listen(inputElement, “keydown”).subscribe({
  next(val) { console.log("Got key: " + val) },
  error(err) { console.log("Got an error: " + err) },
  complete() { console.log("Stream complete!") },
});
複製程式碼

使用 observable 物件。

subscribe() 函式返回了一個訂閱物件。這個物件具有取消訂閱的方法。

Observable 不應混淆於 已廢棄的 Object.observe 函式,Object.observe 是可以觀察物件變化的一種方法。其已被 ECMAScript 2015 中更通用的的實現 Proxy 所替代。

Observable 目前處於 stage 1,但它已被 TC39 委員會標記為 “ready to advance” 並獲得了瀏覽器廠商的大力支援,因此有望很快推進到下一階段。現在你就已經可以開始使用這一提案的特性了,有三種 polyfill 實現 可供選擇。

do 表示式

CoffeeScript 曾因以一切皆為表示式 而名聲大噪,儘管它的流行程度已經衰減,但是它對 JavaScript 近期的發展產生了重大影響。

do 表示式提出了一種將多個語句包裝在一個表示式中的新語法。可以以如下方式編寫程式碼:

let activeToDos = do {
  let result;
  try {
    result = fetch('/todos');
  } catch (error) {
    result = []
  }
  result.filter(item => item.active);
}
複製程式碼

do 表示式示例。

do 表示式的最後一個語句將作為完成值,被隱式地返回。

do 表示式在 JSX 中非常有用。與複雜的三元表示式不同,do 表示式可以使得 JSX 中的流程控制更可讀。

const FooComponent = ({ kind }) => (
 <div>
   {do {
     if (kind === 'bar') { <BarComponent /> }
     else if (kind === 'baz') { <BazComponent /> }
     else { <OtherComponent /> }
   }}
 </div>
)
複製程式碼

JSX 的未來?

Babel 已有外掛 可轉譯 do 表示式。此提案目前處於 stage 1,關於如何與 generator 和 async 函式一起使用,還存在一些重要的開放問題,因此規範可能會發生重大變化。

可空(Optional)屬性的鏈式呼叫 Optional Chaining

受 CoffeeScript 啟發而來的又一個提案是 optional chaining,它帶來了一種訪問物件屬性的簡單方法,面對值有可能為 undefined 和 null 的物件屬性無需使用冗長的三元運算子了。它與 CoffeeScript 的存在操作符類似,但是缺少一些值得注意的特性,比如範圍檢查和可選賦值。

示例:

a?.b // 如果 `a` 是 null/undefined 則返回  undefined,否則則返回 `a.b` 的值
a == null ? undefined : a.b // 使用三元表示式
複製程式碼

提案目前處於 stage 1,已有名為 babel-plugin-transform-optional-chaining 的 Babel 外掛實現。TC39 委員會在2017 年 10 月的最後一次會議 中對它的語法表示擔憂,但是其仍有可能被採納。

全域性物件標準化

編寫能在每個環境中執行的 JavaScript 程式碼並不容易。在瀏覽器中,全域性物件是 window —— 除非處於 web worker 中,此時全域性物件是它本身。在 NodeJS 中則為 global,但是這是在 V8 引擎之上新增的東西,並不是規範的一部分,所以直接在 V8 引擎中執行程式碼時,global 物件不可用。

待標準化提案 提出了可以在所有引擎和執行環境中使用的全域性物件。提案目前處於 stage 3,因此不久便會被採納。

以及更多

TC39 委員會正在審議五十多項活躍提案,其中還未包括二十多個處於 stage 0 尚未推進的提案。

你可以在 TC39 的 GitHub 頁面 檢視活躍提案列表。可以在 stage 0 提案模組找到一些更粗略的想法,包括了像方法引數裝飾器和新的模式匹配語法

也可以在會議記錄議程 倉庫瞭解委員會的優先事項和目前正在處理的問題。演講資料也陳列在會議記錄中,如果你對演講有興趣,也可以進行查閱。

在最近的幾次 ECMAScript 修訂中有幾個重要的語法修改提案,這似乎也是一種趨勢。ECMAScript 正在加快變革腳步,每年都有越來越多的提案,2018 版本似乎會比 2017 版本採納更多的提案。

今年,在語言中新增改善“生活質量”的提案規模較小,如內建物件的型別檢查和給 Math 模組新增度數與弧度助手提案。這類提案將新增到標準庫中,而非修改語法。它們容易進行 polyfill,有助於減少第三方庫的使用。由於無需改變語法,所以很快就可以使用,在提案階段花費的時間也較少。

新的語法固然優秀,但我更希望未來可以見到更多這種型別的提案。JavaScript 經常被人詬病缺少優秀的標準庫,但很明顯大家正在努力去改變。


插語: LogRocket,Web 應用的 DVR

[譯] 那些好玩卻尚未被 ECMAScript 2017 採納的提案

LogRocket 是一個前端日誌記錄工具,它可以讓你像在自己的瀏覽器中一樣重播問題。無需再猜測錯誤發生的原因或是向使用者索要截圖和日誌轉存,LogRocket 允許重播會話來快速定位錯誤源頭。無論使用什麼框架,LogRocket 可以在任意應用中使用,並擁有從 Redux,Vuex 和 @ngrx/store 中記錄上下文的外掛。

除了可以將 Redux 的 action 和 state 記入日誌,LogRocket 還可以記錄控制檯日誌,JavaScript 報錯,呼叫棧資訊,網路請求、響應頭和實體資訊,瀏覽器的元資訊和自定義日誌。它還利用 DOM 在記錄頁面上的 HTML 和 CSS,即使是最複雜的單頁面應用程式,也可以重新繪製畫素級完美的視訊。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章