iT邦幫忙

2026 iThome 鐵人賽

DAY 30
1
JavaScript

30 天新世代 JavaScript 自我學習指南系列 第 30 篇

Day 30|重新學一次 JavaScript 之後:如何面對新的 API 與 AI?

  • 分享至 

  • xImage
  •  

摘要

30 天沿著資料走了一趟: 學到了同步資料流、非同步資料流、資料到手之後怎麼存與複製,最後是不要了怎麼停。一路帶出三個問題: 誰推進、資料是共用還是複製、不要了由誰通知收尾~把這些思維帶著走,遇到下一支新 API 時也能自己拆解問題。

前置知識:讀過系列任一段落即可,算是一篇總回顧,也有一些未來的 ES 提案。

學習路線:Day 09 收束同步 Iterator、Day 19 收束非同步資料流;今天收束整個系列,並整理系列沒寫到的 ES2026 API,以及已確定進入 ES2027 的提案。

標籤:系列總結 ES2023 ES2024 ES2025 ES2026 ES2027 Baseline

https://ithelp.ithome.com.tw/upload/images/20261004/20145251fcc9sWEdl9.png


今日學習目標

  1. 說出 Day 01 那個問題的答案:for...of 怎麼知道下一個值在哪裡。
  2. 用同步資料流、非同步資料流、資料到手之後、不要了怎麼停這四段,回顧 30 天的 API 各自在哪個位置。
  3. 說出 return()、cancel()、abort() 的共同模式:知道「不要了」的一方只能通知,另一端要接,工作才會停。
  4. 用「誰推進、共用還是複製、誰收尾」三個問題,拆解一支沒學過的新 API,例如 Iterator.concat()。
  5. 分清標準年份和實際支援,並知道哪些提案已確定進入 ES2027。

一、回到 Day 01 的問題

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。後面二十幾天在做的事,其實是發現同一個形狀在不同地方反覆出現。


二、30 天的脈絡:資料從哪裡來、怎麼處理、何時停

整個系列其實是跟著資料走一趟:

階段 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 換了名字,責任分工沒變,學到了新東西但其實之前已經默默相遇過。


三、還沒探討到的新 API:ES2026 與 ES2027

受限於篇幅,這 30 天沒辦法把每一支新 API 都寫進來。這一節就來看看還有哪些沒探討到的:先是已經成為標準的 ES2026,再來是已經確定進入 ES2027 的提案。

看之前先提醒一件事,也算是我在學習中查資料發現的小陷阱:

標準年份和能不能用是兩件事。例如 Array.fromAsync() 寫進 ES2026,但瀏覽器 2024 年就支援了。

下表的「支援」欄是依據 2026-10-03 的 Baseline 資料。

  • 「都支援了,但很新」表示 Chrome、Edge、Firefox、Safari 都已支援,只是時間還不長,專案若要相容舊版瀏覽器就得留意;
  • 「還有瀏覽器不支援」 則表示暫時不適合直接用在正式專案。

ES2026:已經是標準,系列沒寫到

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,名字一樣,卻是兩個不同的函式。

  1. instanceof Error 問的是「這個物件的原型鏈上,有沒有這一個 Error.prototype」
  2. iframe 建立的 Error,原型鏈上是 iframe 自己的那一份,答案當然是 false。

這不是新問題。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 的情境~


ES2027:已進入 Stage 4 的提案

下面這些提案在 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 像是會自動熄燈的房間:只要離開 { } 區塊,它就會幫你收尾。用法分兩步:

  1. 把收尾動作寫進物件的 [Symbol.dispose]() 方法。
  2. 用 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,答案就是「離開區塊的那一刻」,不再靠人記得。

算是滿新滿特別的用法~


四、回到 AI

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,先別急著記它怎麼呼叫;先問它站在哪個舊機制上、誰推進、資料共用還是複製、不要了由誰收尾。


最後的感想:紀錄指針與永久指針

https://ithelp.ithome.com.tw/upload/images/20261003/20145251AUrFc5pwWd.png

寫到最後一篇,我一直想到《航海王》的偉大航路。

偉大航路的磁場很亂,一般的指南針沒有用,只能靠紀錄指針:船要在一座島上停留一段時間,讓指針記下這座島的磁場,它才會指向下一座島。島有大有小,要停多久也不一樣,但一座都不能跳過。

這 30 天學習對我來說就是這樣,一開始對終點藏著什麼其實很茫然,但每天都會選擇停留一座島:

Symbol.iterator、await、ReadableStream、AbortSignal,每一篇都是一座島。真正讓指針「記下來」的,不是讀完文件那一刻,而是自己寫範例、看它印出和預期不一樣的順序、回頭查規格,再把錯的地方改掉。

AI 比較像永久指針:它永遠指向一個固定的目的地,可以直接開過去。這很方便,我寫這個系列時也一直在用。但直達的代價是,中間的島都沒有上岸,指針裡也沒有記下任何一座島。

拿到答案,和知道答案為什麼成立,是兩件事。AI 可以告訴我終點長什麼樣子,但程式還沒跑,就先感覺到「這段是 lazy」「這個 signal 沒傳到底」「這個 iterator 已經被讀過了」,這種需要經驗累積一樣的預判,只能靠自己一座島一座島練出來。

新的 ES 版本每年都會帶來新的島,希望這 30 天留下的,不是一張背好的地圖,而是一支已經記下不少磁場的紀錄指針。

謝謝你讀到這裡~


參考資料


上一篇
Day 29|誰該擁有 AbortController?從 TanStack Query 看非同步工作的生命週期
系列文
30 天新世代 JavaScript 自我學習指南 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言