[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. テスト用・疑似データ生成
    • 無限シーケンスや擬似乱数列を生成して、テスト用データを簡単に作る

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


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

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

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

コメント

コメントする

CAPTCHA


目次