2024:ECMAScript 2024——分組與迭代輔助器#
事件#
2024年6月,ECMAScript 2024(ES15) 發佈,主軸是資料分組、迭代器輔助器與非同步程式設計的精緻化。
新語法與特性#
1. Object.groupBy / Map.groupBy#
- 為何加入:依條件分組(把清單依類別分桶)以前要手寫 reduce:
arr.reduce((acc, x) => { (acc[x.type] ??= []).push(x); return acc }, {})——樣板冗長且易錯。 - 理論原因:對應 SQL 的 GROUP BY 與函數式語言的 groupBy(Haskell、Scala 傳統);
Object.groupBy回傳 null-prototype 物件(避免原型污染),Map.groupBy支援任意鍵型別。 - 實用原因:報表、清單分類、UI 分組渲染是高頻需求。
- 彌補缺陷:分組操作的 reduce 樣板與原型污染陷阱。
2. 迭代器輔助器(Iterator helpers:map、filter、take、drop、flatMap、reduce、toArray)#
- 為何加入:陣列有 map/filter(ES5),但惰性迭代器沒有——對無限序列(生成器)或大資料流,
arr.filter().map()會先建出中間陣列,記憶體與時間都浪費。 - 理論原因:直接實現惰性求值(lazy evaluation)與組合子(combinator)理論——Haskell/Rust(
Iterator::map)的迭代器鏈傳統;運算是惰性鏈式組合,只在toArray或for...of時求值。 - 實用原因:大檔案串流處理、分頁 API 抓取(無限序列)需要惰性組合。
- 彌補缺陷:迭代器無法組合、只能 eager 求值的缺陷。
3. Promise.withResolvers#
- 為何加入:「建立 Promise 並在外面控制 resolve/reject」以前要寫
let resolve, reject; const p = new Promise((res, rej) => { resolve = res; reject = rej; })的樣板——變數宣告與 Promise 建構分離且作用域繞路。 - 實用原因:把回呼式 API 包裝成 Promise、事件驅動的單次等待(等某事件發生再繼續)是常見需求。
- 彌補缺陷:Promise 建構的「揭露樣板」(revealing constructor pattern)樣板程式碼。
4. 可調整大小的 ArrayBuffer 與可增長的 SharedArrayBuffer#
- 為何加入:
ArrayBuffer建立後大小固定,動態緩衝(如串流接收)要先估算大小或重新配置。 - 理論原因:對應 C 的
realloc與 Rust 的Vec增長語義。 - 實用原因:WebAssembly 記憶體管理、串流資料緩衝。
- 彌補缺陷:緩衝區大小固定的限制。
5. Atomics.waitAsync#
- 為何加入:
Atomics.wait是同步阻塞(會凍結執行緒),主執行緒不可用;非同步版本讓主執行緒可以等待共享記憶體變化而不凍結。 - 實用原因:Worker 與主執行緒的同步協調(互斥鎖的非阻塞等待)。
- 彌補缺陷:共享記憶體等待只有同步版本的缺陷。
6. String.prototype.isWellFormed / toWellFormed#
- 為何加入:偵測/修正孤立代理對(lone surrogates,無效 Unicode)以前要手寫檢查。
- 實用原因:字串傳輸(JSON、URL)遇到無效代理對會出錯,先
toWellFormed()可防禦。 - 彌補缺陷:Unicode 有效性檢查工具缺失。
理論總結#
ES2024 的主軸是「惰性求值進入主流」:迭代器輔助器把 Haskell/Rust 的惰性組合傳統帶進 JavaScript,是繼 async/await 之後最大的一次範式補充;groupBy 則把 SQL 的分組思維語言化。
實用總結#
Object.groupBy 迅速取代手寫 reduce 分組;Promise.withResolvers 成為回呼→Promise 包裝的標準寫法。
相關條目#
程式範例#
1. Object.groupBy / Map.groupBy:分組一行搞定#
const users = [
{ name: "Ada", role: "admin" },
{ name: "Alan", role: "user" },
{ name: "Grace", role: "admin" }
];
// ES2023 之前:手寫 reduce(樣板冗長、易錯)
const byRole = users.reduce((acc, u) => {
(acc[u.role] ??= []).push(u); // 還要用到 ES2021 的 ??=
return acc;
}, {});
// ES2024:groupBy 一行(null-prototype 物件,無原型污染)
const byRole = Object.groupBy(users, (u) => u.role);
// { admin: [Ada, Grace], user: [Alan] }
// Map.groupBy:支援任意鍵型別(物件、函數當鍵)
const byRank = Map.groupBy(users, (u) => u.name.length);
2. 迭代器輔助器:惰性求值進入主流#
// 陣列方法是 eager:中間陣列全部建出來(記憶體浪費)
[1, 2, 3, 4, 5].filter((x) => x % 2 === 0).map((x) => x * 10);
// 先建 [2, 4],再建 [20, 40]——兩個中間陣列
// ES2024:迭代器輔助器——惰性鏈式組合(Haskell/Rust 傳統)
function* naturals() { let i = 0; while (true) yield i++; } // 無限序列!
naturals()
.filter((x) => x % 2 === 0)
.map((x) => x * 10)
.take(3) // 只取前3個——不會無限跑
.toArray(); // [0, 20, 40]——此時才求值
// drop / flatMap / reduce 也內建
naturals().drop(100).first?.(); // 跳過前100個
3. Promise.withResolvers:回呼→Promise 的標準寫法#
// ES2023 之前:揭露樣板(宣告與建構分離、作用域繞路)
let resolve, reject;
const p = new Promise((res, rej) => { resolve = res; reject = rej; });
button.onclick = () => resolve("clicked");
await p;
// ES2024:一行
const { promise, resolve, reject } = Promise.withResolvers();
button.onclick = () => resolve("clicked");
const result = await promise;
4. 可調整大小的 ArrayBuffer 與 Atomics.waitAsync#
// ES2023 之前:ArrayBuffer 大小固定
const buf = new ArrayBuffer(8); // 永遠 8 bytes,要改大小只能重新配置
// ES2024:可調整大小(C 的 realloc / Rust 的 Vec 語義)
const buf = new ArrayBuffer(8, { maxByteLength: 1024 });
buf.resize(64); // 動態增長
// Atomics.waitAsync:主執行緒的非阻塞等待(wait 會凍結執行緒)
const { async, value } = Atomics.waitAsync(arr, 0, 0);
await async; // Worker 更新共享記憶體後喚醒,不凍結