next() 與 看過基本 Iterator Helper API;。ES2025 Iterator Helpers Lazy Evaluation Pipeline
filter() 等 helper 仍可能讀取多筆來源資料。map()、filter()、take()、drop() 與 flatMap() 的 pull 行為。
Day 06 我們已經看過基本的 Iterator Helper 寫法:
Iterator.from(source)
.map(...)
.filter(...)
.take(3)
.toArray();
今天不再逐一背 map()、filter()、take() 這些 API 的名稱。
而是真正要探索的是:
當 consumer 想要一筆結果時,這條 pipeline 裡程式碼大概發生了什麼事?
理解這件事後,才會知道 lazy 的意思不是「完全不做事」,而是「沒有人要資料時,不主動做事」。
先看一條還沒有被消費的 pipeline:
function* numbers() {
for (let i = 1; i <= 10; i++) {
yield i;
}
}
const result = numbers()
.filter((x) => x % 2 === 0)
.map((x) => x * 10)
.take(3);

這裡其實已經呼叫了 numbers(),但呼叫 Generator 函式只會先取得一個 iterator,不會立刻執行函式本體。因此 for 迴圈還沒開始、還沒有任何 yield 出來的數字;filter() 與 map() 的 callback 也都還沒執行。
它們只是先組裝這個流程管線:
source → filter → map → take → consumer
但資料真正開始流動時,有兩個相反方向:
consumer ──要求一筆結果──▶ take ──▶ map ──▶ filter ──▶ source
consumer ◀──交付一筆值──── take ◀── map ◀── filter ◀── source
第一條是 pull:下游 consumer 要求資料時,每個節點才向自己的上游要一筆。
第二條是 value flow:source 有值後,值才依序通過各個 helper,回到 consumer。
這就是整篇文章最重要的心智圖:
請求由下游往上游走;資料由上游往下游走。
可以把 map()、filter()、take() 理解成一層層等待 pull 的 lazy iterator wrapper。建立 pipeline 時,它們只記住各自的上游與處理規則;直到 consumer 呼叫最外層 iterator 的 next(),才逐層要求資料並執行 callback。
標準的 Iterator Helpers 已經幫我們提供 filter()、map()、take()。不過,先用 Generator 寫出概念版,當然實際內部引擎的實作不一定是這樣,但可以更清楚看見每一層 wrapper 怎麼連接 source 與 consumer,還有程式碼實際的運作思維:
先假裝我第一次看到這些串接步驟,我會用什麼思路開始設計:
iterator,不然資料就會被一次取完。lazyMap(source, mapFn) // 回傳 iterator
lazyFilter(source, filterFn) // 回傳 iterator
take(source, countFn) // 回傳 iterator
take(
lazyMap(
lazyFilter(iterator, filterFn),
mapFn,
),
3,
);
filter() 的規則是:可能向上游拿很多筆,直到找到一筆能交付的值。
function* lazyFilter(source, filterFn) {
for (const value of source) {
if (filterFn(value)) {
yield value;
}
}
}
map() 的規則很單純:向上游拿一筆、轉換、交付。
function* lazyMap(source, mapper) {
for (const value of source) {
yield mapper(value);
}
}
take() 的規則是:交付到指定筆數後,停止向上游拿新資料。
function* take(source, count) {
if (count <= 0) {
return;
}
let taken = 0;
for (const value of source) {
yield value;
taken += 1;
if (taken === count) {
return;
}
}
}
最後把它們接起來:
const pipeline = take(
lazyMap(
lazyFilter(numbers(), (x) => x % 2 === 0),
(x) => x * 10,
),
3,
);
這幾個 function 雖然已經被呼叫,但它們都是 Generator function;呼叫後只會各自回傳一個 iterator,函式本體與裡面的 for...of 都還沒開始執行。
直到 consumer 要第一筆結果:
console.log(pipeline.next());
最外層的 take() 才會開始執行它的 for...of,向 lazyMap() 要資料;lazyMap() 再向 lazyFilter() 要資料;最後 lazyFilter() 才向 numbers() 要資料。
pipeline.next() // consumer 調用幾次 跑幾次 pipeline
→ take 的 for...of 開始讀 lazyMap
→ lazyMap 的 for...of 開始讀 lazyFilter
→ lazyFilter 的 for...of 開始讀 numbers
→ numbers yield 一筆值
當值回來時,predicate、mapper 才會依序執行,結果最終由 take() 交付給 consumer。
next() 跑一次啟動拉取資料把剛才的例子加上 log:
function* numbers() {
for (let i = 1; i <= 10; i++) {
console.log("source produce:", i);
yield i;
}
}
const result = numbers()
.filter((x) => {
console.log("filter:", x);
return x % 2 === 0;
})
.map((x) => {
console.log("map:", x);
return x * 10;
})
.take(3);
console.log(result.next());
輸出會是:
source produce: 1
filter: 1
source produce: 2
filter: 2
map: 2
{ value: 20, done: false }
把這段輸出放回剛才的心智圖,就能看見完整過程:
1. consumer 呼叫 result.next()
2. take 向 map 要一筆結果
3. map 向 filter 要一筆結果
4. filter 向 source 要一筆值
5. source 產生 1
6. filter 拒絕 1,還不能交付結果
7. filter 再向 source 要一筆值
8. source 產生 2
9. filter 放行 2
10. map 把 2 轉成 20
11. take 交付 20
12. consumer 得到 { value: 20, done: false }
注意第 6 步:consumer 雖然只呼叫了一次 next(),但 filter() 為了找出第一個符合條件的值,已經向 source 拉了兩筆資料。
所以 lazy 不能理解成「一次只跑一個步驟」,而是:
沒有下游請求時不執行;有一個下游請求時,只做足以交付那一筆結果的工作。
filter() 特別容易讓人看見 pull?map() 的情況通常很直覺:從上游拿一筆、轉換一筆、交付一筆。
filter() 不一樣。前一節已看到它可能略過 1、直到 2 才交付第一筆結果。如果條件更晚才成立,這個差異會更明顯:
const onlyFive = Iterator.from([1, 2, 3, 4, 5])
.filter((x) => x === 5);
console.log(onlyFive.next());
// { value: 5, done: false }
外層只有一次 onlyFive.next(),但 filter 必須讀完 1 到 5 找到符合條件的值,才能交付第一筆結果。
這不是 eager;它仍然是 lazy。因為那些讀取都發生在 consumer 真的要求第一筆結果之後,不是建立 onlyFive 的時候。
回到前面的 result:
console.log(result.next());
console.log(result.next());
接下來會依序找出來源中的 4 與 6,並交付 40、60。
第二次 next():source 3 被 filter 丟掉 → source 4 通過 → 得到 40
第三次 next():source 5 被 filter 丟掉 → source 6 通過 → 得到 60
現在 take(3) 已經交付三筆結果。即使 source 還能繼續產生 7、8、9,後續的 result.next() 只會得到:
{ value: undefined, done: true }
它不必再向 source 要資料。這也是 take() 有價值的原因:它把「最多需要幾筆」放進 pipeline,讓上游不需要白做更多工作。
以下 ES2025 Iterator helpers 在呼叫當下都不會讀取 source iterator;它們都回傳新的 iterator,等下游真正要資料時才開始工作。
| Helper | consumer 要一筆時,可能發生什麼事? |
|---|---|
map() |
向上游取一筆,轉換後交付一筆。 |
filter() |
可能向上游取多筆,直到找到符合條件的值。 |
drop(n) |
第一次交付前,會先取走並丟掉最多 n 筆。 |
take(n) |
最多交付 n 筆;額度用完後不必再向上游取值。 |
flatMap() |
取到一筆上游資料後,可能把它展開成多筆下游資料。 |
例如 drop(2) 不會在建立時立刻跳過資料:
const afterTwo = Iterator.from([1, 2, 3, 4]).drop(2);
直到 afterTwo.next(),它才會真的讀取並捨棄 1、2,然後交付 3。
因此這些 API 的共同點不是「每次只讀一筆」,而是:
先描述資料怎麼處理;等有人要結果,才只讀取當下必需的資料。
Lazy 不代表 pipeline 的排列順序無關緊要。例如先 filter() 再 map(),mapper 只會處理通過條件的值:
const result = source
.filter(isEligible)
.map(expensiveTransform)
.take(3);
如果先 map() 再 filter(),被 filter 丟掉的值也已做過轉換。兩種順序的結果是否相同,仍取決於 callback 的語意;不能只為了效能任意交換,但可以把較能縮小資料量的步驟放在前面評估。
這個特性在來源很大、昂貴,或根本沒有盡頭時特別重要。
function* ids() {
let id = 1;
while (true) {
yield id++;
}
}
const firstThreeUsers = ids()
.filter((id) => id % 2 === 0)
.map((id) => `user-${id}`)
.take(3)
.toArray();
console.log(firstThreeUsers);
// ["user-2", "user-4", "user-6"]
ids() 可以永遠產生資料,但這段程式只需要讀到 6。
不是因為 JavaScript 先建立了一個無限大的陣列再切前三筆;而是 toArray() 開始要求結果後,take(3) 在拿到三筆資料時就讓整條 pipeline 停下來。
可以不用死背每個 Helper 的內部細節,先記住這張圖:
consumer ──pull request──▶ lazy helpers ──▶ source
consumer ◀───value──────── lazy helpers ◀── source
map()、filter()、take()、drop()、flatMap() 都是 lazy transformations:
下一篇要補上最後一塊:既然 transformation 只是等待下游 pull,那 toArray()、reduce()、some()、find() 這些操作,為什麼一呼叫就會讓整條 pipeline 真的跑起來?