跳轉到

2017:ECMAScript 2017——async/await#

事件#

2017年6月,ECMAScript 2017(ES8) 發佈。最大亮點是 async/await,這是 JavaScript 非同步程式設計的終極範式。

新語法與特性#

1. async/await#

  • 為何加入:Promise(ES6)解決了回呼地獄,但 .then().then().catch() 鏈仍不像同步程式碼,且在條件分支與迴圈中難以組合。
  • 理論原因:源自 C# 5(2012) 的 async/await 設計(微軟 Anders Hejlsberg 團隊),其底層是 協程(coroutine) 與狀態機理論——await 把函數暫停,完成後恢復。JavaScript 的實作將 async 函數降級為 generator + Promise 狀態機。
  • 實用原因:const data = await fetch(url) 與同步寫法一樣直觀;try/catch 可以直接捕捉非同步錯誤(Promise 鏈做不到的統一錯誤處理)。
  • 彌補缺陷:Promise 鏈「不像同步程式碼」的可讀性缺陷,以及非同步錯誤處理無法用 try/catch 統一的缺陷。

2. 共享記憶體與 Atomics(SharedArrayBuffer、Atomics)#

  • 為何加入:Web Worker 之間無法共享記憶體,只能用 postMessage 複製資料。
  • 理論原因:引入正式的記憶體模型(memory model)——ECMAScript 首次定義多執行緒的 happens-before 關係,借鑑 Java Memory Model。
  • 實用原因:高效能計算(WebAssembly、影像處理、遊戲引擎)需要零複製的共享記憶體。
  • 彌補缺陷:Worker 間資料只能複製的效能缺陷。(2018年因 Spectre 漏洞一度停用,2020年以 COOP/COEP 安全條件恢復。)

3. Object.values / Object.entries#

  • 為何加入:ES5 只有 Object.keys,取值或鍵值對要手寫迴圈。
  • 實用原因:Object.entries 與 Object.fromEntries(2019)對稱,是物件↔陣列轉換的標準路徑。
  • 彌補缺陷:物件遍歷 API 的不完整。

4. 字串補白(padStart / padEnd)#

  • 為何加入:格式化(對齊、補零)以前要手寫迴圈或 regex。
  • 實用原因:日期補零('05'.padStart(2, '0'))、表格對齊是最常見需求。
  • 彌補缺陷:字串格式化工具缺失。

5. 函數參數尾逗號(trailing commas)#

  • 為何加入:物件與陣列字面量早允許尾逗號,函數參數不允許造成 diff 噪音。
  • 實用原因:版本控制 diff 只顯示「新增一行」而非「修改一行」。
  • 彌補缺陷:語法規則不一致與維護摩擦。

理論總結#

ES2017 的主軸是「非同步的終局」:從回呼(1995)→ Promise(2015)→ async/await(2017),歷經22年,JavaScript 終於有了既強大又可讀的非同步範式。借用 C# 的成熟設計而非自行發明,是「語言間彼此學習」的典範。

實用總結#

async/await 與後來的頂層 await(2022)徹底改寫了 Node.js API 的設計方向——Deno(2020)甚至直接以「全 Promise API」為核心(見 2020-Deno-1-0問世)。

相關條目#

程式範例#

1. async/await:非同步程式碼像同步一樣直觀#

// ES6 Promise 鏈:可讀但不像同步碼
function loadUser() {
  return fetch("/api/config")
    .then((res) => res.json())
    .then((cfg) => fetch(cfg.userUrl))
    .then((res) => res.json())
    .then((user) => fetch(user.avatarUrl))
    .then((res) => res.json());
}

// ES2017:async/await——三次請求寫成三行同步風格
async function loadUser() {
  const cfg = await (await fetch("/api/config")).json();
  const user = await (await fetch(cfg.userUrl)).json();
  const avatar = await (await fetch(user.avatarUrl)).json();
  return avatar;
}

2. try/catch 統一捕捉非同步錯誤(Promise 鏈做不到)#

async function load() {
  try {
    const res = await fetch("/api");   // 網路錯誤
    const data = await res.json();     // 解析錯誤——同一個 try 捕捉
  } catch (err) {
    console.error(err);                // 所有非同步錯誤都在這裡
  } finally {
    hideLoading();                     // 無論成敗都執行
  }
}

3. 迴圈與條件中 await(Promise 鏈很難寫)#

async function loadPages(pages) {
  const results = [];
  for (const p of pages) {
    if (p.enabled) {                         // 條件分支中 await——鏈式寫法幾乎不可能
      results.push(await (await fetch(p.url)).json());
    }
  }
  return results;
}

4. SharedArrayBuffer 與 Atomics:Worker 間共享記憶體#

// 主執行緒
const sab = new SharedArrayBuffer(8);
const arr = new Int32Array(sab);
worker.postMessage(sab);              // 零複製共享(以前 postMessage 是複製)

// Worker 執行緒
Atomics.add(arr, 0, 1);               // 原子操作,借用 Java Memory Model 語義

5. 字串補白與尾逗號#

"5".padStart(2, "0");     // "05"——日期補零
"7".padEnd(3, ".");       // "7.."
function f(a, b,) {}      // 參數尾逗號:diff 只顯示「新增一行」