30 天沿著資料走了一趟: 學到了同步資料流、非同步資料流、資料到手之後怎麼存與複製,最後是不要了怎麼停。一路帶出三個問題: 誰推進、資料是共用還是複製、不要了由誰通知收尾~把這些思維帶著走,遇到下一支新 API 時也能自己拆解問題。
前置知識:讀過系列任一段落即可,算是一篇總回顧,也有一些未來的 ES 提案。
學習路線:Day 09 收束同步 Iterator、Day 19 收束非同步資料流;今天收束整個系列,並整理系列沒寫到的 ES2026 API,以及已確定進入 ES2027 的提案。
標籤:系列總結 ES2023 ES2024 ES2025 ES2026 ES2027 Baseline

for...of 怎麼知道下一個值在哪裡。return()、cancel()、abort() 的共同模式:知道「不要了」的一方只能通知,另一端要接,工作才會停。Iterator.concat()。Day 01 留了一個問題:
for (const value of [1, 2, 3]) {}
for (const value of new Set([1, 2, 3])) {}
for (const value of 'hello') {}
Array、Set、String 完全不同,為什麼 for...of 都看得懂?
現在可以用三句話回答:
for...of不認識 Array 或 Set。它只找Symbol.iterator這個入口,拿到 iterator 後一次呼叫一次next()(Day 02–03)。如果中途break,它會透過 IteratorClose 嘗試呼叫return(),通知來源可以收尾(Day 03)。
這個答案只用到 Day 02–03。後面二十幾天在做的事,其實是發現同一個形狀在不同地方反覆出現。
整個系列其實是跟著資料走一趟:
| 階段 | Day | 主要 API | 帶出的問題 |
|---|---|---|---|
| 同步資料流 | 02–09 | Iterable、Generator、Iterator Helpers | 誰推進下一步? |
| 非同步資料流 | 10–19 | Promise、await、Async Iterator、Web Streams、SSE、Array.fromAsync() |
資料需要等待時,誰推進? |
| 資料到手之後 | 20–26 | Object/Map/Set、groupBy()、Set 運算、toSorted()、structuredClone()、Uint8Array、Base64 |
共用、複製,還是移交? |
| 不要了怎麼停 | 27–29 | AbortController、AbortSignal.timeout()/any()、TanStack Query |
誰通知、誰停止收尾? |
AbortSignal,是我寫完資料模型 Map/Set/Unit8Array 階段回頭看才發現的:
其實早在 Day 03 的 for...of 和 Day 14 的 ReadableStream,它就各出現過一次類似的訊號,只是換了名字,剛好工作上也有遇到類似問題,索性花點時間研究:
| 層次 | 誰知道「不要了」 | 怎麼通知 | 誰真的停 | 回到哪一天 |
|---|---|---|---|---|
| 同步 Iterator | for...of 的 consumer |
break → IteratorClose → return() |
來源自己,前提是有實作 return() |
Day 03 |
| ReadableStream | reader 或 for await...of |
cancel();break 預設也會 cancel |
underlying source 的 cancel |
Day 14 |
| 非同步工作 | 持有 controller 的一方 | controller.abort() |
有接 signal 的 API,例如 fetch |
Day 27–29 |
三次都是同一個結論:
知道「不要了」的一方只能發出通知;真的停不停,要看另一端有沒有接。
API 換了名字,責任分工沒變,學到了新東西但其實之前已經默默相遇過。
受限於篇幅,這 30 天沒辦法把每一支新 API 都寫進來。這一節就來看看還有哪些沒探討到的:先是已經成為標準的 ES2026,再來是已經確定進入 ES2027 的提案。
看之前先提醒一件事,也算是我在學習中查資料發現的小陷阱:
標準年份和能不能用是兩件事。例如
Array.fromAsync()寫進 ES2026,但瀏覽器 2024 年就支援了。
下表的「支援」欄是依據 2026-10-03 的 Baseline 資料。
「都支援了,但很新」表示 Chrome、Edge、Firefox、Safari 都已支援,只是時間還不長,專案若要相容舊版瀏覽器就得留意;「還有瀏覽器不支援」 則表示暫時不適合直接用在正式專案。| API | 標準 | 一句話 | 和系列的關係 | 支援(2026-10) |
|---|---|---|---|---|
Iterator.concat() |
ES2026 | 把多個 iterable 接成一個,仍然 lazy | Day 06–08 的延伸,第四節會拆解 | 都支援了,但很新 |
Map.prototype.getOrInsert()、getOrInsertComputed() |
ES2026 | 有就拿、沒有就放 | Day 21 分組的逐筆版本,見下方 | 都支援了,但很新 |
Math.sumPrecise() |
ES2026 | 精確加總一串數字 | 接收 iterable,Day 02 的入口 | 都支援了,但很新 |
JSON.parse source text access |
ES2026 | reviver 可以拿到原始文字,大數字不失精度 | Day 24 的序列化邊界 | 都支援了,但很新 |
Error.isError() |
ES2026 | 跨 realm 也能判斷是不是 Error | Day 27–28 的 abort reason,見下方 | 還有瀏覽器不支援 |
getOrInsert():資料一筆一筆來時的分組Day 21 的 Map.groupBy() 要先拿到全部資料,才能一次分組。但如果資料是一筆一筆抵達的,例如 Day 15 的 SSE 事件,就只能邊收邊放。以前要先檢查 key 在不在:
const byRoom = new Map()
function onMessage(msg) {
if (!byRoom.has(msg.roomId)) {
byRoom.set(msg.roomId, [])
}
byRoom.get(msg.roomId).push(msg)
}
ES2026 的 getOrInsert() 把「有就拿、沒有就放」合成一步:
function onMessage(msg) {
byRoom.getOrInsert(msg.roomId, []).push(msg)
}
key 已經存在,就回傳原本的值,不會覆蓋;不存在,才放入預設值並回傳它。
這裡藏著一個 Day 05 的問題:預設值是 eager 的。 JavaScript 呼叫函式之前,會先把每個參數都算好。所以 getOrInsert(key, []) 不管 key 在不在,[] 都會先被建立出來;key 已經存在時,這個新 Array 就白做了。
[] 很便宜,白做也無所謂。
但如果預設值要花時間計算,就改用
getOrInsertComputed():它收的不是「值」,而是「怎麼算出值」的 callback,只有 key 不存在時才會呼叫。用一個會印出訊息的函式來看差別:
function createList(roomId) {
console.log(`建立 ${roomId} 的新陣列`) // 假設這一步很花時間
return []
}
const byRoom = new Map([['frontend', []]]) // frontend 已經存在
// getOrInsert:createList() 在呼叫前就先執行了
byRoom.getOrInsert('frontend', createList('frontend'))
// 印出「建立 frontend 的新陣列」,但 key 已存在,這個陣列沒被用到
// getOrInsertComputed:傳進去的是函式,需要時才執行
byRoom.getOrInsertComputed('frontend', createList)
// 什麼都沒印:key 已存在,callback 沒有被呼叫
byRoom.getOrInsertComputed('backend', createList)
// 印出「建立 backend 的新陣列」:key 不存在,才真的建立並放進 Map
callback 會收到 key 當參數,所以 createList 可以直接傳進去,不用另外包一層 arrow function。
Math.sumPrecise() 與 JSON.parse 的 source:數字不再走樣Math.sumPrecise() 會精確加總一串數字,而且接收任何 iterable,Array、Set 都能放:
[0.1, 0.2, 0.3].reduce((a, b) => a + b) // 0.6000000000000001
Math.sumPrecise([0.1, 0.2, 0.3]) // 0.6
JSON.parse 的 reviver 則多了第三個參數 context,可以拿到數字的原始文字。
後端回傳超過安全範圍的大數字 id 時,就能在轉成 Number 失真之前,先改用 BigInt 接住(Day 24 的序列化邊界):
const text = '{"id": 12345678901234567890}'
JSON.parse(text).id // 12345678901234567000,後幾位已經走樣
JSON.parse(text, (key, value, context) =>
key === 'id' ? BigInt(context.source) : value
).id // 12345678901234567890n
Error.isError():catch 到的東西,真的是 Error 嗎?Day 28 提過,abort() 可以帶自訂 reason,而 fetch() 會直接用這個 reason reject。reason 可以是任何值:
const controller = new AbortController()
controller.abort('user left')
try {
await fetch(url, { signal: controller.signal })
} catch (err) {
console.log(err) // 'user left',是字串
console.log(err.message) // undefined
}
所以在 catch 裡讀 err.message 之前,最好先確認它是不是 Error。平常會寫 err instanceof Error,但 instanceof 檢查的是原型鏈,有兩種情況會判斷錯:
Error.isError(new TypeError('x')) // true
Error.isError(new DOMException('', 'AbortError')) // true,Day 28 的 AbortError 也算
Error.isError('user left') // false
const fake = { __proto__: Error.prototype }
fake instanceof Error // true,原型鏈對了
Error.isError(fake) // false,它不是 Error constructor 建立的
// iframe 或 Node.js vm 裡建立的 Error
iframeError instanceof Error // false,那是另一個 realm 的 Error
Error.isError(iframeError) // true
realm,可以想成
「一整套獨立的 JavaScript 執行環境」:它有自己的全域物件,也有自己的一套內建物件,包括Array、Object、Error。每個 iframe 都有自己的 realm,Node.js 的vm也能建立新的 realm。
所以主頁面的 Error 和 iframe 裡的 Error,名字一樣,卻是兩個不同的函式。
instanceof Error 問的是「這個物件的原型鏈上,有沒有這一個 Error.prototype」這不是新問題。Array 早就遇過一模一樣的狀況,所以才有 Array.isArray():
const iframe = document.createElement('iframe')
document.body.append(iframe)
const IframeArray = iframe.contentWindow.Array
const arr = new IframeArray()
arr instanceof Array // false,原型鏈上是 iframe 的 Array.prototype
Array.isArray(arr) // true
Error.isError() 就是 Error 版的 Array.isArray()。它不看原型鏈,而是看物件身上有沒有 Error constructor 建立時留下的內部欄位,所以跨 realm 也判斷得出來,也不會被假造的物件騙過。
實務上,同一個頁面裡的程式用 instanceof Error 通常就夠了;Error.isError() 的價值主要在錯誤收集工具、跨 iframe 的錯誤回報這類跨 realm 的情境~
下面這些提案在 2026 年已經進入 Stage 4,確定會寫進 ES2027,API 不會再改,只差瀏覽器跟上:
| 提案 | API | 一句話 | 和系列的關係 | 支援(2026-10) |
|---|---|---|---|---|
| Explicit Resource Management | using、await using |
離開區塊就自動收尾 | 第二節的「誰收尾」,見下方 | 尚未 Baseline |
| Joint Iteration | Iterator.zip()、Iterator.zipKeyed() |
多個 iterator 同步前進,每次各取一筆 | Day 06–08 的延伸 | 尚未 Baseline |
| Iterator Chunking | chunks()、windows() |
每 n 筆切成一組,或用滑動視窗讀取 | Day 06–08 的延伸 | 尚未 Baseline |
| Iterator Includes | includes() |
問 iterator 有沒有某個值,找到就停 | Day 08 的消費型操作;比較規則同 Day 22 的 SameValueZero | 尚未 Baseline |
| Iterator Join | join() |
不先轉成 Array,直接串成字串 | Day 08 的消費型操作 | 尚未 Baseline |
| Temporal | Temporal.* |
取代 Date 的日期時間 API |
— | 尚未 Baseline |
光 Iterator 就有四個提案。Day 02–09 花了不少篇幅探索的 Iterator 與 lazy 資料流,還在繼續長;也許哪天在專案裡遇到,就會再次相遇 😂
現在先知道它們都和 Iterator 的資料操作有關,用法也和 Array 很像:includes()、join() 本來就是 Array 有的方法,chunks()、zip() 則類似 lodash 的 chunk()、zip()。
差別在於 Iterator 版本是逐筆拉取,不必先把資料整批放進 Array(Day 09)。
還在 Stage 3 以前的提案這裡不列,因為名稱和行為都可能再改,可以看 TC39 proposals。
using,把收尾寫進語法S2027 的 using 像是會自動熄燈的房間:只要離開 { } 區塊,它就會幫你收尾。用法分兩步:
[Symbol.dispose]() 方法。using(取代 const)宣告這個物件。function showSpinner() {
spinner.hidden = false
return {
[Symbol.dispose]() {
spinner.hidden = true // 收尾動作,寫在這裡一次就好
},
}
}
async function save(data) {
using loading = showSpinner()
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) })
} // 離開函式時,自動呼叫 loading[Symbol.dispose]()
不管 save() 是正常結束、fetch 失敗丟出錯誤,還是中途 return,離開函式時都會呼叫 [Symbol.dispose](),spinner 一定會關掉。
[Symbol.dispose]() 聽起來很新,但它和 Day 03 的 return() 是同一個概念:都是「不要了,請收尾」的通知。差別只在誰能用、誰來呼叫:
return() |
[Symbol.dispose]() |
|
|---|---|---|
| 誰能用 | iterator 專用(Day 03) | 任何需要收尾的物件 |
| 誰來呼叫 | for...of 提早離開時 |
using 離開區塊時 |
而且兩者是接在一起的:內建 iterator 的 [Symbol.dispose](),裡面做的事就是呼叫 return()。所以同一個 Generator,用 for...of 或 using 都會收尾:
function* readLines() {
try {
yield 'line 1'
yield 'line 2'
} finally {
console.log('cleanup')
}
}
for (const line of readLines()) { break }
// for...of 提早離開 → return() → 印出 cleanup
{
using lines = readLines()
lines.next()
}
// 離開區塊 → [Symbol.dispose]() → return() → 印出 cleanup
以前只有 for...of 會自動幫你呼叫 return();如果自己一筆一筆呼叫 next(),中途不讀了,就得自己記得收尾。有了 using,手動讀取也不用再記了。
回到第二節的問題:誰知道不要了? 用了
using,答案就是「離開區塊的那一刻」,不再靠人記得。
算是滿新滿特別的用法~
Day 01 寫過:AI 很快就能寫出「filter iterator 後取前三筆」的程式,但我仍想能判斷三件事。
假設 AI 給了這段程式,而且用的是以前不常寫過的 API
Iterator.concat():
function* loadRows() {
console.log('loading from server...')
yield* fetchRowsSync() // 假設這一步很花時間
}
const firstThree = Iterator.concat(cachedRows, loadRows())
.filter(row => row.active)
.take(3)
.toArray()
就算沒讀過 concat() 的文件,現在也能一一回答:
| Day 01 的問題 | 現在的回答 |
|---|---|
| 它是 eager 還是 lazy? | lazy。toArray() 之前只是接管線,什麼都沒執行。 |
| 會不會一次把資料全讀進記憶體? | 不會。一次只拉一筆,拿到三筆就停;如果 cachedRows 裡就有三筆 active,loadRows() 的本體根本不會執行。 |
| iterator 為什麼用過一次就沒有資料? | iterator 保存的是讀取進度,不是資料。進度走到底就沒了。 |
這是我這 30 天最想得到的東西。
Day 01 畫過一條學習流程:
看到需求 → 問 AI 或查 MDN → 看範例 → 寫進專案 → 能跑就繼續往下
現在很多時候「寫進專案」這件事應該大部分都由 AI 自動完成了....
程式能跑、測試也過,但正確不等於適合:同一段程式,換一個資料量、換一個使用情境,就可能變成問題。
所以 AI 實作完之後,我想能看出哪裡是關鍵點:
| 關鍵點 | 先問自己 | 情境不同,答案就不同 |
|---|---|---|
| 效能 | 誰推進?eager 還是 lazy? | 資料少,用 Array 一次處理最清楚;資料多、只要前幾筆,lazy 才省得下工作(Day 06、09) |
| 資料取用 | 共用、複製,還是移交? | 要更新 state,用 toSorted() 產生新版本;大量 bytes 要交給 Worker,移交比複製省(Day 23–25) |
| 生命週期 | 不要了,誰收尾? | 使用者切換頁面時,signal 有沒有一路傳到 fetch,舊請求才停得下來(Day 29) |
上面那段 Iterator.concat() 也一樣:如果 cachedRows 通常就夠用,lazy 寫法能省下一次昂貴的讀取;但如果每次都得讀完全部資料,直接用 Array 反而更好懂。AI 寫的版本沒有錯,只是它不知道你的資料長什麼樣子。
AI 給的答案不一定錯,但我現在知道要從哪裡判斷,它適不適合眼前這個情境。
這個系列一開始說,不想寫成「Day 1 介紹 API A、Day 2 介紹 API B」。回頭看,三十天裡出現的新 API,幾乎都不是憑空出現的:
| 舊的語言機制 | 新的需求 | 新的 API |
|---|---|---|
| Iterable/Iterator | 想要 lazy 串接 | Iterator Helpers、Iterator.concat() |
| Promise/Async Iterator | 想要收集、想要外部兌現 | Array.fromAsync()、Promise.withResolvers() |
return()/cancel() |
想要不靠 for...of 也能收尾 |
using |
新 API 會一直出現,明年還會有 ES2027、ES2028。但只要先找到它站在哪個舊機制上,它就不再是一個要背的名字。
讀一支新 API,先別急著記它怎麼呼叫;先問它站在哪個舊機制上、誰推進、資料共用還是複製、不要了由誰收尾。

寫到最後一篇,我一直想到《航海王》的偉大航路。
偉大航路的磁場很亂,一般的指南針沒有用,只能靠紀錄指針:船要在一座島上停留一段時間,讓指針記下這座島的磁場,它才會指向下一座島。島有大有小,要停多久也不一樣,但一座都不能跳過。
這 30 天學習對我來說就是這樣,一開始對終點藏著什麼其實很茫然,但每天都會選擇停留一座島:
Symbol.iterator、await、ReadableStream、AbortSignal,每一篇都是一座島。真正讓指針「記下來」的,不是讀完文件那一刻,而是自己寫範例、看它印出和預期不一樣的順序、回頭查規格,再把錯的地方改掉。
AI 比較像永久指針:它永遠指向一個固定的目的地,可以直接開過去。這很方便,我寫這個系列時也一直在用。但直達的代價是,中間的島都沒有上岸,指針裡也沒有記下任何一座島。
拿到答案,和知道答案為什麼成立,是兩件事。AI 可以告訴我終點長什麼樣子,但程式還沒跑,就先感覺到「這段是 lazy」「這個 signal 沒傳到底」「這個 iterator 已經被讀過了」,這種需要經驗累積一樣的預判,只能靠自己一座島一座島練出來。
新的 ES 版本每年都會帶來新的島,希望這 30 天留下的,不是一張背好的地圖,而是一支已經記下不少磁場的紀錄指針。
謝謝你讀到這裡~
using 與 iterator 的 dispose 行為:Explicit Resource Management 提案(%IteratorPrototype%[Symbol.dispose] 呼叫 return())、MDN — using
Iterator.concat():Iterator Sequencing 提案、MDN — Iterator.concat()
getOrInsert():Upsert 提案、MDN — Map.prototype.getOrInsert()
Error.isError():Error.isError 提案、MDN — Error.isError()