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 }); // 保留原始錯誤,不再字串拼接
}