跳轉到

2022:ECMAScript 2022——頂層 await 與類別欄位#

事件#

2022年6月,ECMAScript 2022(ES13) 發佈,主軸是頂層 await、類別私有欄位與「陣列負索引」。

新語法與特性#

1. 頂層 await(Top-level await)#

  • 為何加入:ESM 模組的頂層無法直接 await,必須包進 (async () => {...})() 的立即執行函數——每個模組的入口都要包一層。
  • 理論原因:ESM 的靜態結構本就支援非同步載入(模組圖本身是 Promise 樹),頂層 await 是把「模組載入即非同步」的既有事實語法化。
  • 實用原因:CLI 工具、測試、設定載入(讀取 config、連資料庫)在模組頂層直接寫同步風格。Deno(2020,見 2020-Deno-1-0問世)率先支援,標準化後 Node 14.8+ 跟進。
  • 彌補缺陷:模組入口的 IIFE 包裝樣板。

2. 類別私有欄位與方法(#count)#

  • 為何加入:ES6 class 完全沒有封裝——所有屬性公開,私有約定只能靠 _ 前綴慣例(毫無強制力)。
  • 理論原因:真正的資訊隱藏(Parnas, 1972)在語言層面的實現;# 前綴由 V8 提案(TypeScript 的 private 只是編譯期檢查,# 是執行期強制)。
  • 實用原因:類別內部狀態(快取、計數器)不該被外部修改;執行期強制的封裝可防止意外破壞。
  • 彌補缺陷:class 無封裝、_ 慣例無強制力。

3. static 欄位與 static 初始化區塊(static {})#

  • 為何加入:靜態屬性以前只能在 class 外面用 Class.prop = ... 賦值(與類別定義分離)。
  • 實用原因:靜態常數、單例初始化、複雜的靜態狀態設定(有 try/catch 需求時)。
  • 彌補缺陷:類別定義與靜態狀態初始化的分離。

4. 類別欄位宣告(class fields)#

  • 為何加入:ES6 class 的屬性必須在建構函式裡用 this.x = ... 賦值,無法「宣告即初始化」。
  • 理論原因:對齊 Java/C#/TypeScript 的欄位宣告語法。
  • 實用原因:React class 元件時代,屬性箭頭函數(自動綁定 this)是著名需求。
  • 彌補缺陷:屬性初始化必須進建構函式的樣板。

5. Array.prototype.at#

  • 為何加入:取最後一個元素要寫 arr[arr.length - 1]——冗長且易錯;負索引是 Python/Ruby 的標準能力。
  • 實用原因:arr.at(-1) 直接取尾元素;字串與 TypedArray 同步支援。
  • 彌補缺陷:負索引缺失與尾元素存取的樣板。

6. Object.hasOwn#

  • 為何加入:判斷自有屬性要靠 Object.prototype.hasOwnProperty.call(obj, key)——直接呼叫 obj.hasOwnProperty(key) 有原型污染風險(物件可能覆寫該方法或以 Object.create(null) 建立無此方法)。
  • 彌補缺陷:原型鏈陷阱與冗長寫法。

7. Error cause(new Error(msg, { cause: err }))#

  • 為何加入:錯誤鏈(捕獲 A 再拋 B)以前要靠自訂屬性或字串拼接保存原始錯誤。
  • 理論原因:對應 Java 的 Throwable.getCause——錯誤因果鏈的標準化。
  • 彌補缺陷:錯誤重新拋出時原始錯誤丟失。

理論總結#

ES2022 的主軸是「封裝補完與人體工學」:# 私有欄位補上 OOP 封裝的最後一塊(距 ES6 class 已七年);頂層 await 完成非同步革命的最後一里。

實用總結#

# 私有欄位與頂層 await 在 Node 與瀏覽器全面可用,標誌著「轉譯器時代」的必要性下降——越來越多語法原生支援,Babel 從必需品變成過渡工具。

相關條目#

程式範例#

1. 頂層 await:模組入口的 IIFE 樣板消失#

// ES2021 之前:模組頂層無法 await,要包 IIFE
(async () => {
  const config = await loadConfig();
  startApp(config);
})();

// ES2022:模組頂層直接 await
const config = await loadConfig();   // 模組載入即等待,語義自然
startApp(config);

2. 私有欄位 #:執行期強制的封裝#

// ES6:class 無封裝,_ 慣例毫無強制力
class Counter {
  constructor() { this._count = 0; }   // 外部照樣能改 _count
}
new Counter()._count = 999;  // 靜默成功——封裝形同虛設

// ES2022:# 私有欄位——執行期強制
class Counter {
  #count = 0;                          // 類別欄位宣告 + 私有
  increment() { return ++this.#count; }
  get value() { return this.#count; }
}
const c = new Counter();
c.increment();        // 1
c.#count;             // SyntaxError——語法層面就禁止外部存取

3. static 欄位與 static 初始化區塊#

class Config {
  static version = "2.0";          // static 欄位:宣告即初始化
  static db;                        // 靜態屬性以前要在 class 外面賦值
  static {                          // static 區塊:複雜初始化、可 try/catch
    try {
      this.db = createConnection(process.env.DB_URL);
    } catch {
      this.db = null;
    }
  }
}
Config.version;  // "2.0"——以前要寫 Config.version = "2.0"(與類別定義分離)

4. Array.at:負索引#

const arr = ["a", "b", "c"];

// ES2021 之前:尾元素要手算
arr[arr.length - 1];    // "c"——冗長易錯

// ES2022:at 支援負索引(Python/Ruby 對照)
arr.at(-1);             // "c"
arr.at(-2);             // "b"
"hello".at(-1);         // "o"——字串同步支援

5. Object.hasOwn 與 error.cause#

// 原型陷阱:obj.hasOwnProperty 可能被覆寫或不存在
const obj = Object.create(null);   // 無 hasOwnProperty 方法!
Object.prototype.hasOwnProperty.call(obj, "x");  // 冗長的安全寫法
Object.hasOwn(obj, "x");           // ES2022:一行、安全

// error.cause:錯誤因果鏈
try {
  parseConfig();
} catch (err) {
  throw new Error("啟動失敗", { cause: err });  // 保留原始錯誤,不再字串拼接
}