iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
JavaScript

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

Day 06|Iterator 也有 map / filter 了:ES2025 Iterator Helpers

  • 分享至 

  • xImage
  •  

摘要

  • 一句摘要:ES2025 為既有的 Iterator 補上可串接的標準資料轉換 API。
  • 前置知識:知道資料由 consumer 逐筆要求;需要時才處理,是 lazy 的常見設計。
  • 學習路線:從 Iterator.from() 開始建立標準 pipeline,觀察 lazy transformation 與 consuming operation 如何分工,最後判斷實務上何時適合使用。
  • 標籤ES2025 Iterator Helpers Iterator.from

今日學習目標

  1. 說明 Iterator Helpers 補上了 Iterator protocol 的哪個缺口。
  2. 使用 Iterator.from() 把 iterable 或 iterator 接上標準 helper API。
  3. 使用 map()filter()take()toArray() 建立一條可執行的 pipeline。

https://ithelp.ithome.com.tw/upload/images/20260909/201452519jlcVQCGhz.png


從 proposal 看設計動機

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


ES2025 Iterator pipeline

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 流程如何閱讀:

  1. Iterator.from(source) 將同步資料來源正規化為可使用標準 helpers 的 iterator。依 TC39 的 GetIteratorFlattenable 規則,它可接收兩種形式:

    • iterable:有可呼叫的 [Symbol.iterator]();例如 Array、Set、Map、字串與 Generator。
    • iterator:有可呼叫的 next(),即使本身沒有 [Symbol.iterator]() 也可以。

    Iterator.from() 處理的是同步來源;AsyncIterable/AsyncIterator 要使用對應的非同步處理方式,不能直接放進這裡。

  2. map() 定義每筆值乘以 3;filter() 保留偶數;take(3) 最多交付三筆。

  3. toArray() 才把交付出的值收集為 [6, 12, 18]

前面三個方法不會立即算出一份新資料,而是各自回傳一個新 iterator,把處理規則一層層接起來。

當下游呼叫 next(),或執行 toArray() 這類 consuming operation 時,資料才會沿著 pipeline 向上游逐筆被要求。

不過細節上注意,take(3) 取得的是 pipeline 最後交付出的三筆,不代表來源一定只會被讀三次。

map() 通常取一筆、轉換一筆;filter() 為了找到一筆符合條件的值,可能得略過多筆來源資料;take() 則在交付指定數量後停止再要新的結果。


小實驗:建立 pipeline 時,callback 還沒有執行

把 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() 後的 10filter() 略過,來源總共讀了三筆,take(2) 才能交付 [20, 30]


小實驗:兩種來源都能進入 pipeline

第一個來源是 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 不會自動重播

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() 已交出 612,所以後來的 toArray() 只收集尚未讀取的 1824。它不會從頭再算一次,也不會回傳完整的 [6, 12, 18, 24];若要重跑,請建立新的來源/iterator。

這也是 Iterator Helpers 和 Array chain 的重要差異:Array 比較像「一份可以反覆走訪的資料」;iterator 比較像「一條目前走到某個位置的讀取流程」。


這套 API 解決了什麼?

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。


實務判斷:什麼時候不該使用 Iterator Helpers?

當資料本來就是小型、有限的 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 的價值。


今日總結

  • Iterator protocol 原本只規定如何用 next() 逐筆取值,沒有統一的 map()filter() 等轉換方法。ES2025 Iterator Helpers 補上這組標準、可鏈結的 API;Iterator.from() 則把 iterable 或普通 iterator 轉接到這組 API。
  • map()filter()take() 會建立新的 lazy iterator,呼叫當下不會讀取來源。直到 consumer 呼叫 next()toArray(),pipeline 才開始逐筆取值;讀過的 iterator 會繼續往前,不會自動從頭重播。
  • 以 API 功能性來分工來歸類:「轉換 helper」會回傳新 iterator,例如 map()filter()take();「consuming operation」會開始讀取 iterator 並回傳最終結果,例如 toArray()reduce()some()find()

下一篇預告

我們已能寫出 map → filter → take。下一篇來探討:這些轉換 helper 在何時真的向上游要求資料,以及為何這使它們能保持 lazy。


參考資料


上一篇
Day 05|資料何時才真的被產生?Iterator 的 pull 與 lazy 模式
下一篇
Day 07|Iterator Helpers:Lazy Pipeline 如何逐筆處理資料?
系列文
30 天新世代 JavaScript 自我學習指南7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言