【JavaScript】ECMAScript 2026 新功能介紹
接續上一篇 ECMAScript 2025,這一篇來看 2026 年 6 月正式通過的 ECMAScript 2026 ( ES17 )。
跟上一版那種「一次補齊一整套 API」比起來,這一版的改動幅度沒那麼大,不過每一個都是在解決以前只能自己刻一段程式碼繞過去的問題,實用度反而蠻高的。
ECMAScript 2026
這次總共有七個新功能:
- Upsert
- Array.fromAsync
- Error.isError
- Math.sumPrecise
- Iterator Sequencing
- Uint8Array to/from Base64
- JSON.parse source text access
Upsert 支援度
Map 和 WeakMap 新增了 getOrInsert() 和 getOrInsertComputed()。名字直翻是「取得或插入」,就是「有這個 key 就回傳它的值,沒有就先塞一個進去再回傳」。
這個模式應該不少人寫過,以前要這樣:
const cache = new Map();
// 一個 key 要查兩次
if (!cache.has(userId)) {
cache.set(userId, []);
}
cache.get(userId).push(record);
現在一行:
const cache = new Map();
cache.getOrInsert(userId, []).push(record);
不過要注意的是,getOrInsert() 的第二個參數一定會先被求值,就算 key 已經存在也一樣。如果那個預設值很貴(例如要跑一段運算或建立大物件),就改用 getOrInsertComputed(),它接一個 callback,只有真的需要插入時才會執行:
const cache = new Map();
// key 已存在的話,這個 callback 完全不會跑
const config = cache.getOrInsertComputed(env, (key) => loadHeavyConfig(key));
// 實際行為
const m = new Map([["a", 1]]);
console.log(m.getOrInsert("a", 9)); // 1,已存在就不覆蓋
console.log(m.getOrInsert("b", 2)); // 2,不存在就插入
console.log([...m]); // [['a', 1], ['b', 2]]
Array.fromAsync 支援度
Array.from() 的非同步版本,可以把 async iterable 收集成一個陣列,回傳的是 Promise。
以前處理 async generator 只能自己跑迴圈:
async function* fetchPages() {
let page = 1;
while (page <= 3) {
const res = await fetch(`/api/items?page=${page++}`);
yield await res.json();
}
}
// 以前
const pages = [];
for await (const page of fetchPages()) {
pages.push(page);
}
現在:
const pages = await Array.fromAsync(fetchPages());
它也跟 Array.from() 一樣接第二個參數當 map 函式,而且可以是 async 的:
const urls = ["/a.json", "/b.json", "/c.json"];
const results = await Array.fromAsync(urls, async (url) => {
const res = await fetch(url);
return res.json();
});
這邊跟 Promise.all() 的差別要分清楚:Array.fromAsync() 是依序跑的,一個做完才做下一個;Promise.all() 是全部同時發出去。要平行處理還是得用 Promise.all()。
Error.isError 支援度
判斷一個值是不是真的 Error 物件。
instanceof Error 有兩個老問題:一是跨 realm(例如 iframe、Web Worker、Node 的 vm)會失效,因為兩邊的 Error 建構函式根本不是同一個;二是隨便一個長得像 Error 的物件都能騙過某些檢查。
console.log(Error.isError(new TypeError("x"))); // true
// 長得很像,但不是真的 Error
console.log(Error.isError({ name: "Error", message: "x" })); // false
// 用 prototype 偽裝也騙不過
const fake = Object.create(Error.prototype);
console.log(fake instanceof Error); // true ← 被騙了
console.log(Error.isError(fake)); // false ← 正確
它跟 Array.isArray() 是同一種東西,看的是內部標記而不是原型鏈,所以跨 realm 也準。
Math.sumPrecise 支援度
把一個 iterable 裡的數字加總,而且結果保證是精確的。
浮點數相加會累積誤差,順序不同結果還會不一樣,這個大家應該都有踩過:
const nums = [1e20, 0.1, -1e20];
// 一般加總:0.1 被 1e20 吃掉了
console.log(nums.reduce((a, b) => a + b, 0)); // 0
// ES2026
console.log(Math.sumPrecise(nums)); // 0.1
它的實作會先在內部用更高的精度算完,最後才轉回 float64,所以結果等同於「先用無限精度加總再四捨五入一次」。
要注意它吃的是 iterable,不是展開的參數,所以是 Math.sumPrecise([1, 2, 3]) 而不是 Math.sumPrecise(1, 2, 3)。
金額計算、統計數據這種對精度敏感的場合就用得上。不過它只解決「加總」這一件事,其他四則運算的精度問題還是得靠 decimal 套件。
Iterator.concat 支援度
提案名稱叫 Iterator Sequencing,實際加的是一個 Iterator.concat() 靜態方法,把多個 iterable 串成一個 iterator,依序輪流輸出。
console.log([...Iterator.concat([1, 2], [3, 4])]); // [1, 2, 3, 4]
看起來跟 [...a, ...b] 一樣,不過差別在於它是惰性的,而且可以接無限序列:
function* naturals() {
let i = 100;
while (true) yield i++;
}
// 先給幾個固定值,後面接一個無限產生器
const ids = Iterator.concat([1, 2, 3], naturals()).take(5).toArray();
console.log(ids); // [1, 2, 3, 100, 101]
這裡串起來的東西如果用展開運算子就直接當掉了,因為它會試著把無限序列全部展開。配上 ES2025 的 Iterator Helpers 一起用,處理串流資料蠻順的。
Uint8Array to/from Base64 支援度
終於有內建的 base64 / hex 轉換了,總共六個方法:toBase64()、fromBase64()、setFromBase64()、toHex()、fromHex()、setFromHex()。
const bytes = new Uint8Array([72, 105]);
console.log(bytes.toBase64()); // "SGk="
console.log([...Uint8Array.fromBase64("SGk=")]); // [72, 105]
console.log(new Uint8Array([255, 0]).toHex()); // "ff00"
console.log([...Uint8Array.fromHex("ff00")]); // [255, 0]
也支援 URL-safe 的變體:
bytes.toBase64({ alphabet: "base64url" });
以前要做這件事,得先用 btoa() 轉,但 btoa() 只吃 Latin-1 字串,處理 binary data 要先把每個 byte 轉成字元再拼起來,很繞而且容易出錯。這段的來龍去脈我在 瀏覽器中的 btoa 是什麼? 有寫過,有興趣可以看看。
JSON.parse source text access 支援度
這個是要解決 JSON 裡的大數字精度問題。
JSON 規格沒有限制數字大小,但 JavaScript 的 number 是 float64,超過 Number.MAX_SAFE_INTEGER 就會失真。所以後端傳一個 64-bit 的 ID 過來,JSON.parse() 一跑就壞掉了:
console.log(JSON.parse('{"id":12345678901234567890}').id);
// 12345678901234567000 ← 尾巴不見了
新的做法是 reviver 函式多收一個 context 參數,裡面的 source 是還沒被轉成數字前的原始文字:
const result = JSON.parse('{"id":12345678901234567890}', function (key, value, context) {
if (key === "id") return BigInt(context.source); // 原封不動的字串
return value;
});
console.log(result.id); // 12345678901234567890n
反過來要輸出的時候,用 JSON.rawJSON() 把一段文字標記成「原始 JSON」,JSON.stringify() 就會照原樣寫出去,不會加引號:
const payload = { id: JSON.rawJSON("12345678901234567890") };
console.log(JSON.stringify(payload));
// {"id":12345678901234567890}
另外還有一個 JSON.isRawJSON() 可以判斷某個值是不是 raw JSON。
以前碰到這種需求,只能在 parse 前先用 regex 把大數字換成字串,超級髒,現在總算有正規做法了。
總結
| 功能 | 解決什麼 |
|---|---|
Map.getOrInsert() | 不用再寫 if (!map.has(k)) map.set(k, ...) |
Array.fromAsync() | async generator 收集成陣列 |
Error.isError() | 跨 realm 也準的 Error 判斷 |
Uint8Array.toBase64() | 不用再繞 btoa() 處理 binary |
JSON.rawJSON() | JSON 大數字不再失真 |