Iterator.from() 開始建立標準 pipeline,觀察 lazy transformation 與 consuming operation 如何分工,最後判斷實務上何時適合使用。ES2025 Iterator Helpers Iterator.from
Iterator.from() 把 iterable 或 iterator 接上標準 helper API。map()、filter()、take() 與 toArray() 建立一條可執行的 pipeline。
JavaScript 早在 ES2015 就有「逐筆取資料」的共同約定:iterator 提供 next(),consumer 便能持續取得 { value, done }。這是之前已介紹過的 迭代協議(Iterator protocol),解決的是「怎麼拿下一筆」。
但協議本身沒有規定每個 iterator 都要有實作 .map()、.filter() 或 .take()。想把資料轉換串起來,過去常得自己包 Generator,或先展開成 Array。
ES2025 的 Iterator Helpers 補上的則是可 chain 的標準 API:解決「如何用共同寫法組合逐筆轉換」。
TC39 的 Iterator Helpers proposal 指出,iterator 很適合表示大型或可能無限的資料,但過去缺少像 Array methods 一樣方便的操作,因此有些原本可以逐筆處理的問題,最後仍被寫成 Array,或必須依賴函式庫。Iterator Helpers 的目的不是取代 Array,而是讓 iterator 保留逐筆讀取的模型,同時獲得標準化、可鏈結的操作方式。
這項提案在合併前已達 Stage 4,現已納入 ECMAScript 2025;proposal repository 也已封存。若要確認正式行為,應以 ECMAScript 2025 的 ECMA-262 規格 為準。
以 Iterator Helpers 來說,現代瀏覽器大致已可使用;MDN 將它標為 Baseline 2025,表示自 2025 年 3 月起,最新版主要瀏覽器均已支援,但舊版瀏覽器或舊 Node.js 仍可能沒有。
其實從 ES2015 開始,內建 iterator 就已經共用一個 prototype;只是過去它不是開發者日常會直接操作的公開 API。ES2025 新增全域 Iterator,讓這個共同基底正式透過 Iterator.prototype 暴露給開發者,並在上面提供 helpers 與 Iterator.from()。(規格將這個共同基底記為 %IteratorPrototype%。)
不過,只有 next() 的自訂 iterator 不會因此自動繼承 Iterator.prototype,也就不會直接有 .map() 等 helpers;可在使用前交給 Iterator.from() 包裝,或讓自訂類別 extends Iterator。
Iterator.from() 會把 iterable 或 iterator 正規化為可使用 helpers 的 iterator。可以先把它想成一個 轉接器(adapter):就像插頭轉接器把不同規格的插頭接到同一種插座。
Iterator.from() 把不同形式的來源接到標準 Iterator API,讓它們可以使用 map()、filter() 與 take() 等 helpers。它不是資料轉換器,也不會把來源複製成 Array。接著,我們就能把轉換描述串起來,最後由 toArray() 取得結果。
const source = [2, 3, 4, 5, 6, 7, 8];
const scores = Iterator.from(source)
.map(value => value * 3)
.filter(value => value % 2 === 0)
.take(3)
.toArray();
console.log(scores);
// [6, 12, 18]
這裡的 source 資料來源是 Array,所以其實也可以先呼叫 .values() 變成 Iterator,再直接使用 helpers,上面範例只是先簡單示範 Iterator 新的標準 API:
const scores = source
.values()
.map(value => value * 3)
.filter(value => value % 2 === 0)
.take(3)
.toArray();
如果程式很確定只處理 Array,.values() 就已經足夠。
範例使用
Iterator.from(),是因為實務上的函式可能收到 Array、Set、Generator,或只有next()的普通 iterator。Iterator.from()可以把這些來源統一接到相同的 helper API;如果手上已經是標準 iterator,則可以直接呼叫 helpers,不必再包一層Iterator.from()。
接著看看上面程式碼像是管路 pipeline 流程如何閱讀:
Iterator.from(source) 將同步資料來源正規化為可使用標準 helpers 的 iterator。依 TC39 的 GetIteratorFlattenable 規則,它可接收兩種形式:
[Symbol.iterator]();例如 Array、Set、Map、字串與 Generator。next(),即使本身沒有 [Symbol.iterator]() 也可以。Iterator.from() 處理的是同步來源;AsyncIterable/AsyncIterator 要使用對應的非同步處理方式,不能直接放進這裡。
map() 定義每筆值乘以 3;filter() 保留偶數;take(3) 最多交付三筆。
toArray() 才把交付出的值收集為 [6, 12, 18]。
前面三個方法不會立即算出一份新資料,而是各自回傳一個新 iterator,把處理規則一層層接起來。
當下游呼叫 next(),或執行 toArray() 這類 consuming operation 時,資料才會沿著 pipeline 向上游逐筆被要求。
不過細節上注意,take(3) 取得的是 pipeline 最後交付出的三筆,不代表來源一定只會被讀三次。
像map() 通常取一筆、轉換一筆;filter() 為了找到一筆符合條件的值,可能得略過多筆來源資料;take() 則在交付指定數量後停止再要新的結果。
把 log 放進來源與 map() callback,可以直接看到執行時機:
function* numbers() {
for (const number of [1, 2, 3, 4]) {
console.log(`source: ${number}`);
yield number;
}
}
const pipeline = Iterator.from(numbers())
.map((number) => {
console.log(`map: ${number}`);
return number * 10;
})
.filter((number) => number >= 20)
.take(2);
console.log(pipeline instanceof Iterator);
// true 代表這邊中間任務處理完後還是 iterator
// 此時 source、map 的 log 都還沒出現
console.log("pipeline ready");
// 目前只印出:pipeline ready
console.log(pipeline.toArray()); // 開始啟動 pipeline 從這裡才開始
// 觀察這裡的中間任務 map 是每提取一筆資料時 才去執行~
// source: 1
// map: 1
// source: 2
// map: 2
// source: 3
// map: 3
// [20, 30]
建立 pipeline 時,JavaScript 只是建立各層 helper iterator;這時 Generator 還沒有交出數字,map() 的 callback 也沒有執行。
呼叫
toArray()後,它才反覆索取下一筆結果。因為1通過map()後的10被filter()略過,來源總共讀了三筆,take(2)才能交付[20, 30]。
第一個來源是 Set,它有 [Symbol.iterator](),因此是 iterable。第二個來源只有 next(),不是 iterable;Iterator.from() 會把它包成可使用 helpers 的 iterator。
const iterableSource = new Set([1, 2, 3]);
console.log(Iterator.from(iterableSource).toArray());
// [1, 2, 3]
const iteratorOnlySource = {
current: 1,
next() {
if (this.current > 3) {
return { done: true };
}
return { value: this.current++, done: false };
},
};
console.log(iteratorOnlySource[Symbol.iterator]);
// undefined
// 經過 Iterator.from 轉換也是可以調用 map 等 helper
console.log(
Iterator.from(iteratorOnlySource)
.map(value => value * 10)
.toArray(),
);
// [10, 20, 30]
重點不是 Iterator.from() 把資料複製成 Array,而是它把兩種形式都接到相同的 iterator API。第二個物件本身仍沒有 .map();可呼叫 .map() 的是 Iterator.from(iteratorOnlySource) 回傳的結果。
Iterator 會記住目前位置;每次 consumer 取得一筆,來源就往前移動。把「建立 pipeline」和「收集結果」拆開看,就能看見這件事:
const source = [2,4,6,8]
const remainingScores = Iterator.from(source)
.map(value => value * 3)
.filter(value => value % 2 === 0);
console.log(remainingScores.next().value); // 6
console.log(remainingScores.next().value); // 12
console.log(remainingScores.toArray()); // [18, 24]
前兩次 next() 已交出 6、12,所以後來的 toArray() 只收集尚未讀取的 18、24。它不會從頭再算一次,也不會回傳完整的 [6, 12, 18, 24];若要重跑,請建立新的來源/iterator。
這也是 Iterator Helpers 和 Array chain 的重要差異:Array 比較像「一份可以反覆走訪的資料」;iterator 比較像「一條目前走到某個位置的讀取流程」。
Iterator Helpers 是「標準化、可鏈結的 iterator API」,不是把 iterator 變成 Array。它的方法可以先分成兩種角色:
| 角色 | 常見方法 | 呼叫後得到什麼 | 何時讀取來源 |
|---|---|---|---|
| lazy transformation | map()、filter()、take()、drop()、flatMap() |
新的 iterator | 當下游索取結果時 |
| consuming operation | toArray()、reduce()、forEach()、some()、every()、find() |
Array、單一值、boolean 或 undefined |
呼叫方法時就開始 |
toArray()、reduce() 與 forEach() 通常會持續讀到 iterator 結束;some()、every() 與 find() 則可能在答案已經確定時提前停止。因此「consuming」不一定代表「總是讀到最後一筆」,而是指這類方法會開始向 iterator 要資料,並回傳最終結果,不再回傳另一層 lazy iterator。
當資料本來就是小型、有限的 Array,而且畫面最後一定需要完整陣列時,Array methods 往往更直接:
const visibleNames = users
.filter(user => user.active)
.map(user => user.name);
這種情境改成 iterator 不一定更快,也可能讓團隊多一個相容性與「來源會被消費」的概念要維護。Iterator Helpers 的價值比較容易出現在資料量大、來源可逐筆計算、可能是無限序列、只需要前幾筆,或想避免建立不必要的中間 Array 時。
因此選型不是「新 API 一律較好」,而是看資料是否已完整存在、最後是否必須 materialize 成 Array,以及專案 target 是否原生支援。已經是小型 Array 的資料,繼續使用 Array methods 通常最清楚;大型、昂貴、可能無限,或只需要前幾筆的來源,才更能顯出 Iterator Helpers 的價值。
next() 逐筆取值,沒有統一的 map()、filter() 等轉換方法。ES2025 Iterator Helpers 補上這組標準、可鏈結的 API;Iterator.from() 則把 iterable 或普通 iterator 轉接到這組 API。map()、filter()、take() 會建立新的 lazy iterator,呼叫當下不會讀取來源。直到 consumer 呼叫 next() 或 toArray(),pipeline 才開始逐筆取值;讀過的 iterator 會繼續往前,不會自動從頭重播。map()、filter()、take();「consuming operation」會開始讀取 iterator 並回傳最終結果,例如 toArray()、reduce()、some() 與 find()。我們已能寫出 map → filter → take。下一篇來探討:這些轉換 helper 在何時真的向上游要求資料,以及為何這使它們能保持 lazy。
%IteratorPrototype%