新しいJavaScript構文においては、ループ処理が強化されています。過去においてループ処理と言えば forwhile を使ったものに限定されていましたが、現在は forEachfilter であったり、 for 〜 of のような構文もあります。

この時の肝になるのがイテレーターとジェネレーターです。

この記事では、この2つの機能の使い方を解説します。

イテレーターについて

イテレーターは以下のような決まりを持った配列です。

  1. next メソッドで次の値を返す
  2. 値は {value: "値", done: "判定"} という形で取得できる
  3. まだ次の値がある場合(最後の配列要素ではない)は、 done プロパティが false
  4. 次の値がない場合(最後の配列要素)は、 done プロパティが true

これを繰り返し呼ぶことによって、ループ処理を行います。

let result = it.next();
while (!result.done) {
 console.log(result.value);
 result = it.next();
}

ただし、このような配列を作るのは面倒であり、高度なプログラミングスキルが求められます。

イテレーターが実装されているクラス

イテレーターは自作もできますが、すでにイテレーターを実装しているクラスを使うことができます。それは以下の2つです。

  • Array
  • Map

例えばArrayは以下のように使えます。

const ary = [1, 2, 3, 4, 5];
for (const value of ary) {
    console.log(value * 2);
}

// => 2, 4, 6, 8, 10

Mapは次のようになります。

const map = new Map;
map.set('one', 1);
map.set('two', 2);
map.set('three', 3);
map.set('four', 4);
map.set('five', 5);

for (const [key, value] of map) {
    console.log(${key} => ${value});
}

上記の結果は次のようになります。

one => 1
two => 2
three => 3
four => 4
five => 5

MapはJavaScriptで一般的に使われるObjectとは別物です。

そのため、以下はエラーになります。

エラーメッセージは obj はイテレータブルではない(つまりイテレーターに対応していない)という内容です。

const obj = {
    one: 1,
    two: 2,
    three: 3,
    four: 4,
    five: 5,
};
for (const value of obj) {
    console.log(value);
}
// => Uncaught TypeError: obj is not iterable

そこで使われるのが、「ジェネレーター」になります。

ジェネレーターについて

ジェネレーターは任意の処理をイテレーターにするための機能です。

function* という構文を使います。

そして yield という構文を使って、イテレータにデータを送ります。

簡単に書くと、次のようになります。

function* test() {
    yield 1
    yield 2
}

const i = test();
for (const value of i) {
    console.log(value);
}

この結果は、以下のようになります。

つまり、ジェネレーターの結果 i をイテレーターとして実行すると、 yield で送っているデータが順番に value に入るという仕組みです。

1
2

このジェネレーターは next メソッドを呼び出す毎に、結果が得られます。

つまり、以下のように書けます。

value なのは一番最初に説明したイテレーターと同じ構造だからです。

function* test() {
    yield 1
    yield 2
    yield 3
}

const i = test();
console.log(i.next().value); // => 1
console.log(i.next().value); // => 2
console.log(i.next().value); // => 3

つまり、ジェネレーターは next メソッドを呼び出すまで、処理を停止できる関数と言い換えることもできます。

ジェネレーターに引数を渡す

ジェネレーターを呼び出す際に、引数を渡せます。

ただし、この引数はジェネレーター側では1つ前の実行結果として受け取る変数になります。

つまり、一番最初の呼び出し時には指定できません。ちょっと分かりづらいですが、以下のような出力になります。

function* test() {
    // message1には「こんにちは」と入る
    const message1 = yield "はじまり"

    // message2には「Monaca」と入る
    const message2 = yield message1

    yield ${message1} ${message2}
}

const i = test();
console.log(i.next().value); // => はじまり
console.log(i.next("こんにちは").value); // => こんにちは
console.log(i.next("Monaca").value); // => こんにちは Monaca

yieldをイテレータブルにする

yieldをイテレータブルにするために yield* という書き方があります。この場合、返す値をイテレータブルにすれば、順番に渡すようになります。

function* test() {
    yield* [0, 1, 2, 3]
}
const i = test();
console.log(i.next().value); // => 0
console.log(i.next().value); // => 1
console.log(i.next().value); // => 2
console.log(i.next().value); // => 3

ちなみに文字 もイテレータブルなので、以下のようになります。

function* test() {
    yield* "Monaca"
}

const i = test();
console.log(i.next().value); // => M
console.log(i.next().value); // => o
console.log(i.next().value); // => n
console.log(i.next().value); // => a
console.log(i.next().value); // => c
console.log(i.next().value); // => a

まとめ

今回はイテレーターとジェネレーターについて解説しました。

イテレーターは配列やMapで使えるので、普段のプログラムの中でもよく使う機能になるでしょう。

ジェネレーターは若干特殊な操作にはなりますが、うまく使うと処理全体が分かりやすく書けるようになります。

ぜひ使い方をマスターして、可読性の高いコードを実現してください。