[JavaScript講座] イテレータ・ジェネレータ

当ページのリンクには広告が含まれています。
目次

イテラブルとイテレータ

イテレータとは?

イテレータ(iterator) はひとことで言うと、「next() メソッドで 次の値を順番に取り出せるオブジェクト」です。

イテレータは、次の形のオブジェクトを返す next() を持ちます:

{ value: 任意の値, done: boolean }
  • done: false → まだ続きがある
  • done: true → もう値は終わり

手作りイテレータの例:

function createCounterIterator(max) {
  let current = 1;
  return {
    next() {
      if (current <= max) {
        return { value: current++, done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const it = createCounterIterator(3);

console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }

イテラブルとは?

イテラブル(iterable) は、イテレータを「生成できる」オブジェクトです。

Symbol.iterator という特別なキーのメソッドを持っていて、それを呼ぶとイテレータが返ってくるオブジェクト」が イテラブル です。

const iterable = {
  [Symbol.iterator]() {
    // ここで「イテレータ」を返す
    let current = 1;
    const max = 3;
    return {
      next() {
        if (current <= max) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

iterable は、Symbol.iterator メソッドを持っているので イテラブル です。
そして、iterable[Symbol.iterator]() が返すオブジェクトが イテレータ です。

どこで使われるのか?

JavaScriptの イテラブルプロトコル は、次のところで使われています。

  • for...of ループ
  • スプレッド構文 ...(配列リテラル、関数引数など)
  • 配列分割代入(const [a, b] = iterable;
  • Array.from(iterable)
  • Promise.all(iterable) など、一部の標準関数の引数

たとえば:

const arr = [10, 20, 30];

for (const value of arr) {
  console.log(value);
}

これは内部的に:

  1. const iterator = arr[Symbol.iterator]();
  2. ループごとに iterator.next() を呼ぶ
  3. done: true になるまで続ける

という動きをしています。

配列だけでなく、文字列・MapSetarguments・多くのDOMコレクション もイテラブルです。

組み込みイテラブルの例と for…of の挙動

配列

const arr = ["a", "b", "c"];

console.log(typeof arr[Symbol.iterator]); // "function"

for (const value of arr) {
  console.log(value); // "a" "b" "c"
}

文字列

const s = "ABC";

for (const ch of s) {
  console.log(ch); // "A" "B" "C"
}

Map / Set

const set = new Set([1, 2, 3]);
for (const v of set) {
  console.log(v); // 1, 2, 3
}

const map = new Map([
  ["a", 1],
  ["b", 2]
]);
for (const [key, value] of map) {
  console.log(key, value); // "a" 1 / "b" 2
}

for...in との違い

  • for...in
    • 「列挙可能なプロパティ名」 を反復する
    • オブジェクト用(キー列挙)
  • for...of
    • 「イテラブルからの値」 を反復する
    • 配列・文字列・Map・Set などに使う
const arr = ["a", "b"];

for (const i in arr) {
  console.log("in:", i); // "0", "1"
}

for (const v of arr) {
  console.log("of:", v); // "a", "b"
}

自作イテラブルを実装してみる

シンプルなカウンターイテラブル

const counter = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;

    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const n of counter) {
  console.log(n); // 1 2 3 4 5
}

ポイント:

  • Symbol.iterator は 毎回「新しいイテレータ」 を返すべき
  • for...ofcounter[Symbol.iterator]() を呼んで、返ってきたイテレータでループしている

無限イテレータ

const infinite = {
  [Symbol.iterator]() {
    let current = 1;
    return {
      next() {
        return { value: current++, done: false }; // doneがfalseのまま
      }
    };
  }
};

// for (const n of infinite) { ... } は無限ループになるので注意
const it = infinite[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }

イテレータ自体は無限でもOK ですが、for...of などで使う場合は
必ずどこかで break する、回数を制限する、など制御が必要です。

ジェネレータ関数とは何か

function*yield

ジェネレータ関数 は function* で宣言します。

function* gen() {
  console.log("start");
  yield 1;
  console.log("between");
  yield 2;
  console.log("end");
}

呼び出し:

const g = gen();
console.log(g.next()); // "start" -> { value: 1, done: false }
console.log(g.next()); // "between" -> { value: 2, done: false }
console.log(g.next()); // "end" -> { value: undefined, done: true }

ポイント:

  • gen() を呼んだ瞬間には 中のコードはまだ実行されない
    • 「実行可能なジェネレータオブジェクト」を返すだけ
  • g.next() を呼ぶたびに、
    • 前回 yield で止まっていたところから再開し、
    • 次の yield まで進んで一旦止まり、
    • { value, done } を返す
  • done: true になったらそれ以上は進まない

つまりジェネレータは、「実行を一時停止・再開できる関数」+「イテレータを簡単に作るための仕組み」です。

ジェネレータは「イテレータかつイテラブル」

function* で作ったジェネレータオブジェクトは、イテレータであると同時に イテラブル でもあります。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();

console.log(typeof g[Symbol.iterator]); // "function"
console.log(g[Symbol.iterator]() === g); // true
  • g[Symbol.iterator]()g 自身を返す
  • つまり for...of にそのまま渡せる
for (const v of gen()) {
  console.log(v); // 1 2 3
}

ジェネレータでイテラブルを簡単に書く

カウンターイテラブルをジェネレータで書き直す

さっきのカウンターイテラブル:

const counter = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

これをジェネレータで書くとこうなります:

const counter2 = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() {   // ← メソッドでも * が付けられる
    for (let n = this.from; n <= this.to; n++) {
      yield n;
    }
  }
};

for (const n of counter2) {
  console.log(n); // 1 2 3 4 5
}
  • *[Symbol.iterator]() の中で yield しているだけ
  • next() オブジェクトを手書きしていた部分が、ほぼ自動で処理される のがポイントです。

ジェネレータで無限シーケンスを定義

function* naturals() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

const it = naturals();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
  • while (true) でも、実際に next を呼ばない限り進まない
  • 「必要になったときにだけ値を計算する(遅延評価)」という性質がわかりやすく現れます

yield と next() の値のやりとり

yield の評価結果

function* gen() {
  const x = yield 1;
  console.log("x =", x);
  const y = yield 2;
  console.log("y =", y);
}

呼び出し:

const g = gen();

console.log(g.next());     // { value: 1, done: false }
console.log(g.next(10));   // 「10」が一つ前のyield式の結果になる
// → x = 10
//   { value: 2, done: false }
console.log(g.next(20));   // → y = 20
//   { value: undefined, done: true }
  • yield で「外側に渡す値」は value
  • 「次の next(arg)arg が 前回の yield 式の評価結果 になる

実務ではここまで凝ったことはあまりしませんが、

  • 「双方向通信が可能」
  • 「イテレータ+コルーチン的な動き」

をするのがジェネレータの強みのひとつです。

returnthrow

  • ジェネレータ内で return value すると、その時点で done: true になり、value が返る
  • ただし for...of など多くの「使用する側」は、done: true のときの value は無視する
function* gen() {
  yield 1;
  return 999;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 999, done: true }
console.log(g.next()); // { value: undefined, done: true }

外から g.throw(error) を呼んでエラーを投げ込むこともできますが、
用途が限られるので、「そういうこともできる」程度の理解で十分です。

ジェネレータの代表的な用途

ここまでの知識をどう使うか、代表的なパターンだけイメージしておきます。

  1. カスタム反復処理
    • 配列やツリー構造(DOM・自作データ)を「特定の順番で」たどりたいとき
    • 例:木構造を前順・後順・幅優先など好みの順で走査
  2. 大きなデータの「遅延評価」
    • 巨大配列を全部メモリに持たず、「必要なぶんだけ順次読み込んで処理」
    • ログファイルを1行ずつジェネレータで読む、など(Node + ストリームと組み合わせ)
  3. 処理パイプラインの実装
    • map/filter 的な処理を、配列に変換せずジェネレータ同士つなげる
    • パフォーマンス重視のライブラリではよく使われるパターン
  4. テスト用・疑似データ生成
    • 無限シーケンスや擬似乱数列を生成して、テスト用データを簡単に作る

「書かないとメリットが分かりにくい」機能ですが、イテレータの基礎理解としては重要 です。

非同期イテレータとは何か

これまで見てきた(同期)イテレータは、

  • obj[Symbol.iterator]() がイテレータを返す
  • そのイテレータの next() が 同期的に { value, done } を返す

というプロトコルでした。

非同期イテレータは、その「非同期版」です。

非同期イテレータのプロトコル

非同期イテレータは、次の2つを満たすオブジェクトです。

  1. [Symbol.asyncIterator]() メソッドを持つ const asyncIterable = { [Symbol.asyncIterator]() { return {/* ここでイテレータを返す */} } };
  2. そのイテレータの next() が Promise を返す const asyncIterator = asyncIterable[Symbol.asyncIterator](); // next() の戻り値は Promise<{ value, done }> const p = asyncIterator.next(); p.then(({ value, done }) => { // ここに到達するタイミングは非同期 });

まとめると、「[Symbol.asyncIterator]() で取り出せて、next() が Promise を返すもの」が非同期イテレータです。

async ジェネレータ(async function*)

非同期イテレータを素手で実装するのは面倒なので、
同期ジェネレータのときと同じく 専用の構文 が用意されています。

// 非同期ジェネレータ関数
async function* genNumbers() {
  let i = 0;
  while (true) {
    await new Promise(r => setTimeout(r, 1000)); // 1秒待つ(非同期)
    yield i++; // 1秒ごとに値を生成
  }
}

この genNumbers() を呼び出すと、

const it = genNumbers(); // 非同期イテレータを返す

// it[Symbol.asyncIterator]() === it (イテレータ自身)
// it.next() は Promise<{ value, done }>

のような 非同期イテレータ になります。

同期ジェネレータとの違い

  • 同期ジェネレータ:function*
    • next() は 即座に { value, done } を返す
  • 非同期ジェネレータ:async function*
    • next() は Promise で { value, done } を返す
    • 中で await が使える
    • yield で値を「非同期に」順番に流せる

イメージとしては、「非同期処理を挟みつつ、1つずつ値を流していくストリーム」を実現するための構文、という感じです。

for await…of の基本

for...of が「同期イテレータ用のループ」だったのに対し、
for await...of は「非同期イテレータ用のループ」 です。

基本形

for await (const value of asyncIterable) {
  // asyncIterable からの値を、順番に非同期で受け取る
}

ここで asyncIterable は:

  • Symbol.asyncIterator を持つ非同期イテラブル
    または
  • (多くの環境では)Symbol.iterator を持つ「普通のイテラブル」
    • この場合、各値がPromiseなら await してくれる

async ジェネレータとの組み合わせ

さっきの genNumbers()for await...of で回す例:

async function main() {
  const it = genNumbers();

  for await (const n of it) {
    console.log(n);
    if (n >= 3) break;
  }
}

main();

動作イメージ:

  1. genNumbers() を呼ぶ → 非同期イテレータ it を得る
  2. for await は内部で
    • it[Symbol.asyncIterator]() を呼び
    • 毎回 await it.next() して { value, done } を取り出す
  3. yield された値が n に入り、ループ本体が実行される
  4. done: true になったらループ終了

同期版との違いは:

  • 各ステップが await 前提になっているので
    「値が準備でき次第」ループが続く、という非同期フローを自然に書ける点です。

現実的なユースケース

API を順番に叩くストリーム風の処理

// 複数ページのAPIを順に叩く非同期ジェネレータ
async function* fetchPages(startPage = 1) {
  let page = startPage;

  while (true) {
    const res = await fetch(`https://example.com/items?page=${page}`);
    if (!res.ok) {
      throw new Error("HTTP error " + res.status);
    }
    const data = await res.json();

    if (data.items.length === 0) {
      return; // ここで done: true
    }

    yield data.items; // 1ページ分の結果を yield
    page++;
  }
}

async function main() {
  try {
    for await (const items of fetchPages()) {
      console.log("ページを取得:", items.length, "件");

      // 必要ならここで break もできる
      if (/* もう十分 */ false) {
        break;
      }
    }
  } catch (e) {
    console.error("取得中にエラー:", e);
  }
}

main();

ここでのポイント:

  • fetchPages() 側は 「次のページを取ってきたら yield」 という自然な書き方
  • 呼び出し側は 「for await…of で順番に受け取る」 だけ

Node.js のストリーム風の使い方

Node.jsの Readable ストリーム(最近のバージョン)にはSymbol.asyncIterator もあります。

import { createReadStream } from "node:fs";

async function main() {
  const stream = createReadStream("big.txt", { encoding: "utf8" });

  for await (const chunk of stream) {
    console.log("チャンク:", chunk.length);
  }

  console.log("読み込み完了");
}
  • for await...of でストリームからのデータをチャンクごとに非同期で受け取る
  • 「読み終わるまで待つ」のではなく、届いた分から順に処理できる

エラー処理と終了処理

try/catch と組み合わせ

for await...ofawait と同じく、内部で起きた reject を throw に変換してくれるので、

async function main() {
  try {
    for await (const v of asyncIterable) {
      console.log(v);
    }
  } catch (e) {
    console.error("ループ中にエラー:", e);
  }
}

のように try/catch でまとめてハンドリングできます。

途中で break / return したとき

for await...of から breakreturnthrow で抜けるとき、

  • ジェネレータ側に return() / throw() が呼ばれて
  • 必要ならクリーンアップ処理(finally 的なもの)を入れられます

たとえば、非同期ジェネレータ内部に try/finally を書いておくと:

async function* gen() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log("クリーンアップ");
  }
}

async function main() {
  for await (const x of gen()) {
    console.log(x);
    break; // ここで抜ける
  }
}
// 実行すると 1 → "クリーンアップ" といった出力になる

<<前へ(Node.js標準モジュール)

>>次へ(メタプログラミング)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次