iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
JavaScript

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

Day 08|哪些 Iterator Helpers 真的會消費 Iterator?

  • 分享至 

  • xImage
  •  

摘要

一句摘要map()filter()take() 只建立 lazy pipeline;直到 toArray()reduce()some() 等 consuming API 被呼叫,資料才真正開始流動。

前置知識:理解 Iterator 的 next(),以及 Day 07 的 lazy pipeline。

學習路線:分清 lazy transformation 與 consuming operation。

標籤ES2025 Iterator Helpers Consuming API Short-circuit


今日學習目標

  1. 分辨 lazy transformation 與 consuming API。
  2. 從程式碼指出是哪一行啟動 iterator 的消費流程。
  3. 解釋 consuming API 如何反覆呼叫最外層 iterator 的 next()
  4. 分辨完整消費與提前停止時關閉流程 short-circuit。

https://ithelp.ithome.com.tw/upload/images/20260911/20145251hZkC7uxY7I.png


資料何時開始被拉取?

先看這段程式:

const pipeline = [1, 2, 3, 4, 5]
  .values()
  .filter((n) => {
    console.log("filter", n);
    return n % 2 === 1;
  })
  .map((n) => {
    console.log("map", n);
    return n * 10;
  })
  .take(2);

console.log("pipeline ready");

輸出只有:

pipeline ready

filter()map()take() 都已經被呼叫,callback 卻還沒執行。因為它們是 lazy transformations:負責把處理步驟接成新的 iterator,呼叫當下還不會向 source 取得元素。

現在加上:

console.log(pipeline.toArray());

才會輸出:

filter 1
map 1
filter 2
filter 3
map 3
[ 10, 30 ]

toArray() 一執行,整條 pipeline 才開始向 source 要資料。

filter()、map()、take()
        ↓
只建立 lazy pipeline

toArray()
        ↓
成為 consumer,開始 pull 資料

這就是程式啟動開始拉取資料和轉換最重要的分界~


Lazy API 與 consuming API

Iterator Helpers 可以先分成兩類。

Lazy transformations

map()
filter()
take()
drop()
flatMap()

它們回傳新的 iterator,讓我們繼續串接 pipeline:

const pipeline = source
  .filter(isActive)
  .map(toProfile)
  .take(3);

執行到這裡,只是把規則準備好,還沒有讀取 source 的元素。


Consuming operations

toArray()
reduce()
forEach()
some()
every()
find()

它們不再回傳另一個 lazy iterator,而是開始讀取 iterator,並取得 Array、值、boolean 或 undefined


toArray():啟動流程並讀到結束

function* numbers() {
  console.log("produce 1");
  yield 1;

  console.log("produce 2");
  yield 2;

  console.log("produce 3");
  yield 3;
}

const iterator = numbers();

console.log("iterator ready");
const result = iterator.toArray();
console.log(result);

輸出:

iterator ready
produce 1
produce 2
produce 3
[ 1, 2, 3 ]

呼叫 numbers() 只取得 Generator iterator,所以先印出 iterator ready。直到 toArray() 被呼叫,才會開始消費。


TC39 如何規定 toArray() 持續取得下一筆?

來稍微看看原始 ECMAScript 規格,其實並不會直接寫一段 JavaScript 的 while 迴圈,而是使用抽象演算法描述這件事,如同我們前面看過迭代步驟規範一樣。

Iterator.prototype.toArray() 先透過 GetIteratorDirect 取得目前 iterator 的紀錄,接著進入 Repeat。每一輪都執行 IteratorStepValue;這個抽象操作會呼叫 iterator 紀錄中的 [[NextMethod]],也就是我們熟悉的 next()

TC39 規格步驟 實際作用 倉庫比喻
GetIteratorDirect(iterator) 記錄 iterator 與它的 next 方法 找到出貨窗口,記住取下一箱的方法
Repeat 持續進行取值流程 收貨員持續回來取貨
IteratorStepValue 索取下一筆並檢查是否完成 向窗口索取下一箱
呼叫 [[NextMethod]] 實際執行 iterator.next() 倉庫交出一箱,或告知已無貨
done === true 停止迴圈並回傳結果 倉庫清空,收貨完成

可以把 toArray() 想成收貨員:它先找到最外層 iterator 的出貨窗口,然後一次索取一筆;只要還沒收到 done: true,就會繼續呼叫 next()
翻成接近 JavaScript 的心智模型,大致如下:

function consumingToArray(iterator) {
  const result = [];

  while (true) {
    const step = iterator.next();

    if (step.done) {
      return result;
    }

    result.push(step.value);
  }
}

這是方便理解的概念版本,不是規格原始碼。正式演算法還包含型別檢查與 abrupt completion 等細節。

如果 receiver 是一條 pipeline:

const pipeline = source
  .filter(...)
  .map(...)
  .take(3);

pipeline.toArray();

toArray() 只會直接向最外層的 take 要值:

take 功能上一篇有提過,很像守門員一樣, take(3) 是最外層的限量閘門,最多向 toArray() 交出三筆結果。toArray() 只直接對 take 索取資料,再由 take 控管向上游的 map、filter 與 source 逐層索取。

toArray 的 IteratorStepValue
→ take.next()
→ map.next()
→ filter.next()
→ source.next()

每個 lazy helper 再向自己的 upstream 要值,因此一個 consuming API 就能啟動整條 pipeline。
所以 toArray() 不只是把結果改成 Array;它也是啟動 lazy pipeline 的 consumer。


大部分 consuming API 都很像 Array API

reduce()forEach()some()every()find() 的用途、callback 寫法與回傳結果,都和熟悉的 Array 版本很接近:

const arrayTotal = [1, 2, 3].reduce(
  (sum, n) => sum + n,
  0,
);

const iteratorTotal = Iterator.from([1, 2, 3]).reduce(
  (sum, n) => sum + n,
  0,
);

console.log(arrayTotal, iteratorTotal);
// 6 6

因此這裡不重新介紹每個 callback 怎麼寫。真正要注意的是資料來源不同:

  • Array API 處理已經存在的集合。
  • Iterator API 從目前位置反覆呼叫 next(),取出的資料同時也被消費。

相同的 API 名字放在 Iterator 上,才會成為啟動 lazy pipeline 的 consumer。


Consuming API 有兩種停止方式

toArray()reduce()forEach():通常讀到結束

function* numbers() {
  for (const n of [1, 2, 3, 4]) {
    console.log("produce", n);
    yield n;
  }
}

const total = numbers().reduce(
  (sum, n) => sum + n,
  0,
);

console.log(total);

輸出:

produce 1
produce 2
produce 3
produce 4
10

reduce() 為了得到最後的 10,必須把 iterator 讀完。toArray() 要收集所有元素,forEach() 要對每一筆執行 callback,所以正常情況下也會讀到 done: true

forEach() 即使回傳 undefined,仍然會消費 iterator。判斷 consuming API 的重點不是「有沒有回傳值」,而是它有沒有開始向 iterator 要資料。

補充:沒有初始值的 reduce() 遇到空 iterator 時,會和 Array 版本一樣拋出 TypeError


some()every()find():答案確定就停止

這三個 API 也會立即開始讀取 iterator,但可以 short-circuit

function* numbers() {
  for (let i = 1; i <= 5; i++) {
    console.log("produce", i);
    yield i;
  }
}

const result = numbers().some((n) => n === 3);

console.log(result);

輸出:

produce 1
produce 2
produce 3
true

找到 3 後,some() 已經知道答案是 true,所以不會再要求 45

const allEven = Iterator.from([2, 4, 5, 6])
  .every((n) => n % 2 === 0);

console.log(allEven);
// false

const found = Iterator.from([10, 20, 30, 40])
  .find((n) => n > 20);

console.log(found);
// 30

every() 讀到第一個不符合條件的 5 就能回傳 falsefind() 找到 30 就能回傳,不需要為了這次查找繼續讀取 40

提前得到答案時,Iterator 可能會被關閉

some()every()find() 提前得到答案後,不會繼續呼叫 next()。它們會在內部執行 IteratorClose,再由 IteratorClose 檢查 iterator 有沒有 return()


take()find() 串在一起時,誰會觸發關閉?

先看這條 pipeline:

const result = source
  .filter(isActive)
  .take(3)
  .find(isTarget);

take(3) 雖然寫在 find() 前面,但它只先建立一個「最多交出三筆」的 lazy iterator,不會先把三筆資料全部取完。

find() 才是最外層的取資料 API。它每要一筆,take 才向上游要一筆,然後把值交回給 find() 判斷:

find() 要一筆
→ take.next()
→ filter.next()
→ source.next()
→ 值逐層回到 find()
→ find() 執行 predicate

如果 predicate 是 falsefind() 才會再開始下一輪索取。因此 take()find() 是逐筆交錯執行,會出現兩種結束情況:

情況一:predicate 在 take 的額度用完前找到答案
→ find() 對 take 執行 IteratorClose
→ take.return() 將關閉往上游傳遞
→ find() 回傳找到的值

情況二:predicate 判斷三筆後仍沒有找到答案
→ find() 索取下一筆
→ take 發現數量上限已用完,關閉上游 iterator
→ take 回報 done: true
→ find() 回傳 undefined

IteratorClose 不是可以串在 find() 後面的 helper,而是 find()take() 在停止取值時可能執行的內部收尾流程,終止流程的這段觸發者不一定是最末端的程式碼呼叫唷。

https://ithelp.ithome.com.tw/upload/images/20260912/20145251psTqVV9NO0.png

return() 在哪裡被呼叫?

結束原因 IteratorClose 關閉誰? 若存在,會呼叫
find() 的 predicate 得到 true find() 直接連接的 take take.return()
take() 額度用完後再次被索取 take 的上游 iterator upstream.return()

把兩條路徑展開來看:

情況一:find 找到答案
find()
→ IteratorClose(take)
→ take.return()
→ 關閉繼續往上游傳遞
→ upstream.return()(若存在)

情況二:take 額度用完
take.next()
→ IteratorClose(upstream)
→ upstream.return()(若存在)
→ take 回報 done: true

find()take() 都不是直接無條件呼叫 return();它們先執行 IteratorClose,再由 IteratorClose 檢查目標 iterator 是否提供 return()


Iterator 只會從目前位置繼續

Iterator 是有狀態的讀取游標。已經讀過的資料,不會因為改用另一個 consuming API 就自動回來。

const iterator = Iterator.from([1, 2, 3, 4]);

console.log(iterator.next());
// { value: 1, done: false }

console.log(iterator.toArray());
// [2, 3, 4]

console.log(iterator.next());
// { value: undefined, done: true }

1 已被第一次 next() 消費,因此 toArray() 只能從 2 開始收集。

這也是使用 Iterator Helpers 時很重要的檢查:

這個 iterator 是否已經被其他 consumer 讀過?


無限 Iterator 必須先加上邊界

function* naturalNumbers() {
  let n = 1;

  while (true) {
    yield n++;
  }
}

下面這段不會完成:

naturalNumbers().toArray();

因為 toArray() 會持續要求資料,但 naturalNumbers() 永遠不會回報 done: true
JavaScript 主執行緒會一直被占用;同時 Array 持續增長,最後通常會因記憶體耗盡而讓程式崩潰。

先使用 lazy 的 take() 限制筆數,才可以安全地轉成 Array:

const result = naturalNumbers()
  .take(5)
  .toArray();

console.log(result);
// [1, 2, 3, 4, 5]

執行順序仍然是:

take(5)     → 建立有上限的 lazy iterator
toArray()   → 開始 pull,取得五筆結果

六個 consuming API 的差異

API 呼叫後開始消費? 會因答案確定而 short-circuit? 回傳值
toArray() Array
reduce() 累積結果
forEach() undefined
some() boolean
every() boolean
find() 找到的值或 undefined

正常完成時:

  • toArray()reduce()forEach() 通常會讀到 iterator 結束。
  • some()every()find() 可以在答案確定後提前停止。
  • callback 或 iterator 的 next() 若拋出錯誤,操作也會中止;這和 short-circuit 是不同情況。

今日總結

看到下面這段:

const result = source
  .filter(isActive)
  .map(toProfile)
  .take(3)
  .toArray();

可以把它讀成:

filter()、map()、take()
→ 建立 lazy pipeline,尚未讀取 source 元素

toArray()
→ 成為 consumer,開始逐筆 pull

取得三筆結果
→ 回傳 Array

今天最重要只是放慢理解這段過程,包括之前談到索取資料資料轉換過程的細節,核心觀念可以收成一句話:

Lazy helpers 負責把流程接起來; 消費型取資料的 API 才真正啟動流程。

使用這些 Iterator Helper 取資料的 API 前,可以快速確認三件事:

  1. 這個 iterator 是否已經被讀過?
  2. source 是否可能是無限的?
  3. 我需要完整結果,還是答案確定後就能提前停止?

參考資料


上一篇
Day 07|Iterator Helpers:Lazy Pipeline 如何逐筆處理資料?
下一篇
Day 09|Iterator 改變我看待程式執行的方式:從整批處理到 Lazy 資料流
系列文
30 天新世代 JavaScript 自我學習指南14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言