Iterator.join 與 Iterator.zip 怎麼用?Chrome 153 新 API 與 fallback
處理 generator、串流或大型資料集時,常見的做法是先用 Array.from() 把 iterator 展開,再呼叫 join() 或用索引把多組資料配在一起。這樣雖然直觀,卻會失去 iterator 的延遲特性,也容易忘記 iterator 只能被消費一次。
Chrome 153 在 2026 年 9 月 8 日的 stable release 中加入 Iterator.prototype.join()、Iterator.zip() 與 Iterator.zipKeyed()。直接答案是:有限資料要轉成文字時用 iterator 的 join();要把多組 iterable 按位置配對時用 Iterator.zip(),已有欄位名稱則優先用 Iterator.zipKeyed()。 但這批 API 目前仍不是所有瀏覽器的穩定基線,正式環境一定要先做 feature detection 和 fallback。
先確認支援邊界
Chrome 官方 release notes 把這三個 API 列為 Chrome 153 的 JavaScript 變更;MDN 目前仍將 Iterator.prototype.join() 標為 experimental。這兩個訊號要一起看:它已經可以在指定的 Chrome 版本測試,不代表 Firefox、Safari、舊版 Chromium 或目前所有 Node.js runtime 都能直接執行。
我在本機的 Node.js v24.13.0 檢查結果是 Iterator 存在,但 Iterator.prototype.join、Iterator.zip 和 Iterator.zipKeyed 尚未提供。因此不要把「本機 Node 能執行其他 iterator helper」當成這三個方法已經可用:
console.table({ iterator: typeof globalThis.Iterator, join: typeof globalThis.Iterator?.prototype?.join, zip: typeof globalThis.Iterator?.zip, zipKeyed: typeof globalThis.Iterator?.zipKeyed,});用能力偵測取代 User-Agent 判斷:
const supportsIteratorJoin = typeof globalThis.Iterator?.prototype?.join === 'function';const supportsIteratorZip = typeof globalThis.Iterator?.zip === 'function';const supportsIteratorZipKeyed = typeof globalThis.Iterator?.zipKeyed === 'function';Iterator.prototype.join():不用先建立陣列
join() 是 iterator instance method,行為接近 Array.prototype.join():依序消費 iterator,把值轉成字串,再用指定分隔符串起來。沒有傳分隔符時,預設使用逗號;null 和 undefined 會變成空字串。
function* changedFiles() { yield 'src/app.js'; yield 'src/styles.css'; yield 'tests/app.test.js';}
const summary = changedFiles().join('、');console.log(summary);// src/app.js、src/styles.css、tests/app.test.js這個寫法少了一個中間陣列,但不是「完全不讀取資料」:join() 必須走完整個有限 iterator 才能得到最後字串,所以它不是 lazy operation。無限 generator 不能直接呼叫 join(),否則程式會一直等待。
Iterator 只能消費一次
iterator 通常是有狀態的物件。第一次 join() 會把值全部取走,第二次再用同一個 iterator 得到的只會是空字串:
const values = [1, 2, 3][Symbol.iterator]();
console.log(values.join('-')); // 1-2-3console.log(values.join('-')); // 空字串如果同一份資料需要產生兩種輸出,保留可重複建立 iterator 的 iterable,或在需求確實需要時保存陣列;不要把已消費的 iterator 傳給另一個處理階段後,才期待它能重新開始。
舊瀏覽器的 join() fallback
最簡單的 fallback 是 Array.from(iterable).join():
function joinIterable(iterable, separator = ',') { const iterator = iterable[Symbol.iterator]();
if (typeof iterator.join === 'function') { return iterator.join(separator); }
// fallback 會把所有值 materialize,適合有限且可接受記憶體成本的資料。 return Array.from(iterator).join(separator);}
console.log(joinIterable(new Set(['a', 'b', 'c']), ' / '));// a / b / c如果資料量很大,應改成逐步寫入輸出 buffer、限制最大筆數,或使用支援 streaming 的資料格式。fallback 的目標是保留功能,不是假裝它和原生 iterator operation 有相同的記憶體特性。
Iterator.zip():按位置配對多組資料
Iterator.zip() 接收一個「由多組 iterable 組成的 iterable」,每次輸出一個陣列:第一組的第一項配第二組的第一項,依此類推。它適合把 API 回傳的 id、名稱和狀態按同一個索引組成列資料:
const ids = [101, 102, 103];const names = ['首頁', '搜尋', '文章'];const statuses = ['ready', 'ready', 'draft'];
const rows = Iterator.zip([ids, names, statuses]);
for (const [id, name, status] of rows) { console.log({ id, name, status });}輸出的 iterator 會在每次 next() 時向各個來源各取一項,不必先把所有欄位轉成同一個大陣列。可是來源 iterable 仍然有副作用時,必須把取值順序和停止時機當成 API 合約測試;這不是把任意非同步資料流自動變成 async iterator。
shortest、longest 和 strict 怎麼選
不同來源長度不一致時,第二個參數的 mode 決定結果:
| mode | 行為 | 適合情況 |
|---|---|---|
shortest(預設) | 任一來源結束就停止,其他 iterator 會被關閉 | 以最短欄位為有效資料範圍 |
longest | 等所有來源結束,較短來源用 padding 補值 | 報表要保留最長欄位的每一列 |
strict | 來源沒有同時結束時丟出 TypeError | 欄位長度不一致代表資料錯誤 |
例如匯入資料時,通常不應該默默截掉多出來的 id,strict 會更安全:
try { const rows = Iterator.zip( [[101, 102, 103], ['首頁', '搜尋']], { mode: 'strict' }, );
console.log([...rows]);} catch (error) { if (error instanceof TypeError) { console.error('欄位長度不一致,停止匯入'); }}若是選 longest,可以明確填入每一組來源的補值:
const rows = Iterator.zip( [[101, 102, 103], ['首頁', '搜尋']], { mode: 'longest', padding: [null, '未命名'] },);
console.log([...rows]);// [ [101, '首頁'], [102, '搜尋'], [103, '未命名'] ]padding 只在 longest 模式有意義;不要設定了 padding 就以為 shortest 會自動補齊。
Iterator.zipKeyed():用欄位名稱避免順序搞錯
如果來源本來就是以欄位名稱分組,zipKeyed() 會輸出物件,而不是必須靠陣列位置記憶欄位:
const columns = { id: [101, 102, 103], name: ['首頁', '搜尋', '文章'], status: ['ready', 'ready', 'draft'],};
for (const { id, name, status } of Iterator.zipKeyed(columns, { mode: 'strict',})) { console.log(`${id}: ${name} (${status})`);}zip() 適合已有陣列欄位或需要用位置處理的資料;zipKeyed() 適合表格、API response 或多個命名欄位。後者的優點不是輸出比較快,而是欄位名稱會留在結果裡,降低把 id 和 status 順序對調的風險。
Progressive enhancement 的實作方式
不要在模組載入時無條件呼叫新 API。可以把支援判斷留在功能邊界,並為不支援的環境提供小而明確的 fallback:
function* zipShortest(iterables) { const iterators = iterables.map((value) => value[Symbol.iterator]());
while (true) { const results = iterators.map((iterator) => iterator.next()); if (results.some((result) => result.done)) return; yield results.map((result) => result.value); }}
function* zipKeyedShortest(columns) { const keys = Object.keys(columns);
for (const values of zipShortest(keys.map((key) => columns[key]))) { yield Object.fromEntries( values.map((value, index) => [keys[index], value]), ); }}
function rowsFromColumns(columns) { if (typeof globalThis.Iterator?.zipKeyed === 'function') { return globalThis.Iterator.zipKeyed(columns, { mode: 'strict' }); }
// 這個 fallback 只實作 shortest,沒有偽造 strict 的錯誤語意。 return zipKeyedShortest(columns);}這個 fallback 只適合你明確接受的簡化需求。若應用程式依賴 strict、longest 的補值、iterator 關閉或完整的例外語意,應選擇經審查的 polyfill/函式庫,並把測試放在支援矩陣中,而不是自行複製半套規格。
上線前至少測這四種情況:
- 新 API 存在與不存在時,主要流程都能完成。
join()的輸入是空 iterator、含null/undefined,以及很大的有限資料。zip()的欄位長度相同、不相同,並分別驗證三種mode。- iterator 被讀過一次後,不會被錯誤地重複傳給第二個 consumer。
常見問題
Q: Iterator.join() 比 Array.from(iterator).join() 一定更快嗎?
A: 不能直接保證。原生 join() 少了中間陣列,但仍然要消費完整 iterator;實際成本取決於資料來源、字串大小和 runtime。它的主要價值是讓 iterator pipeline 不必為了串接文字而先改成陣列。
Q: Chrome 153 stable 支援,就可以不做 fallback 嗎?
A: 不行。Chrome release notes 只代表指定 Chrome channel 的支援;MDN 仍標示部分 API 為 experimental,而且其他瀏覽器與 Node.js 版本可能尚未提供。以 feature detection、你的瀏覽器支援矩陣和實際測試決定是否啟用。
Q: zip() 和 zipKeyed() 該選哪個?
A: 已經有陣列欄位、結果也適合用陣列解構時選 zip();來源是命名欄位時選 zipKeyed()。如果長度不一致是資料錯誤,兩者都建議使用 mode: 'strict',不要讓預設的 shortest 靜默截斷資料。
參考資料:
回報錯字、失效連結,或告訴我你想看的延伸主題。