跳轉到

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 更新共享記憶體後喚醒,不凍結