2018:ECMAScript 2018——非同步迭代#
事件#
2018年6月,ECMAScript 2018(ES9) 發佈,主軸是非同步迭代、物件其餘/展開屬性與正則表達式大升級。
新語法與特性#
1. for await...of 與非同步迭代器#
- 為何加入:
for...of(ES6)只能迭代同步集合;串流處理(逐行讀檔、分頁 API)需要迭代「逐步產生的非同步值」。 - 理論原因:擴充迭代器協議——
Symbol.asyncIterator讓迭代協議支援 Promise,是協程理論的延伸。 - 實用原因:Node 的串流(streams)與分頁抓取可用
for await (const chunk of stream)自然表達。 - 彌補缺陷:非同步序列無法用迴圈迭代的缺陷。
2. 物件的其餘/展開屬性(...)#
- 為何加入:陣列的展開/其餘在 ES6 已有,物件版本一直缺席(要用
Object.assign複製)。 - 理論原因:補齊語法對稱性——陣列與物件應有一致的展開語義。
- 實用原因:React/Redux 時代「不可變更新」是核心操作,
{...state, count: state.count + 1}成為標準寫法。 - 彌補缺陷:物件複製要靠
Object.assign的冗長與淺拷貝陷阱。
3. Promise.prototype.finally#
- 為何加入:Promise 鏈的成功與失敗要分別處理,「無論成敗都要做的事」(關閉 loading、釋放資源)要寫兩遍。
- 理論原因:對應 try/catch/finally 的 finally 語義——資源清理的正統位置。
- 實用原因:UI 的 loading 狀態切換是每個請求的必做項。
- 彌補缺陷:Promise 缺少 finally 等價物的不完整。
4. 正則表達式大升級#
- 命名捕獲群組
(?<year>\d{4}):以前要用數字索引(match[1]),命名群組可讀性大增,借鑑 Python/.NET 的正則語法。 - Lookbehind 斷言
(?<=...)、(?<!...):補齊 lookahead 的對稱缺口。 s旗標(dotAll):.能匹配換行,以前要靠[\s\S]駭客寫法。- Unicode 屬性跳脫
\p{Script=Greek}:國際化文字處理的標準路徑。 - 彌補缺陷:ES3 正則(1999)20年來的語法缺口總補齊。
理論總結#
ES2018 的主軸是「補齊對稱性」:物件補上陣列已有的展開/其餘、正則補上各語言早已有的命名群組與 lookbehind、Promise 補上 try/finally 的等價物。
實用總結#
物件展開運算子成為不可變資料範式(React/Redux)的語言級支柱,是使用頻率最高的 ES2018 特性。
相關條目#
程式範例#
1. for await...of:逐步迭代非同步序列#
// 逐行讀取串流(Node streams / 分頁 API)
async function* lines(path) {
const stream = fs.createReadStream(path);
for await (const chunk of stream) { // ES2018:迭代非同步值
yield chunk.toString();
}
}
for await (const line of lines("/var/log/app.log")) {
console.log(line);
}
2. 物件展開/其餘:不可變更新的語言支柱#
// ES2017 之前:Object.assign 複製(冗長)
const next = Object.assign({}, state, { count: state.count + 1 });
// ES2018:物件展開(React/Redux 標準寫法)
const next = { ...state, count: state.count + 1 };
// 其餘屬性:解構時收攏剩下的
const { password, ...safeUser } = user; // 移除密碼欄位再回傳
3. Promise.finally:無論成敗的清理#
showLoading();
fetch("/api")
.then((res) => res.json())
.then(render)
.catch(showError)
.finally(hideLoading); // 以前要在 then 和 catch 各寫一次 hideLoading()
4. 正則大升級#
// 命名捕獲群組:可讀性大增
"2026-10-08".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/).groups;
// { year: "2026", month: "10", day: "08" }——以前要用 match[1], match[2]
// Lookbehind:補齊 lookahead 的對稱
"$100".match(/(?<=\$)\d+/); // ["100"]——前面是 $ 才匹配
"a1 b2".match(/\d+(?!px)/); // lookahead 對照
// s 旗標(dotAll)
"line1\nline2".match(/line1.line2/s); // 匹配成功——以前 . 不吃換行
// Unicode 屬性跳脫
"日本語".match(/\p{Script=Japanese}/u); // 國際化文字處理