跳至主要内容

【JavaScript】ECMAScript 2026 新功能介紹

接續上一篇 ECMAScript 2025,這一篇來看 2026 年 6 月正式通過的 ECMAScript 2026 ( ES17 )。

跟上一版那種「一次補齊一整套 API」比起來,這一版的改動幅度沒那麼大,不過每一個都是在解決以前只能自己刻一段程式碼繞過去的問題,實用度反而蠻高的。

ECMAScript 2026

這次總共有七個新功能:

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 大數字不再失真

參考資料