JavaScriptジェネレーター

概要: このチュートリアルでは、JavaScriptジェネレーターとその効果的な使い方について学びます。

JavaScriptジェネレーター入門

JavaScriptでは、通常の関数は、実行完了モデルに基づいて実行されます。途中で一時停止したり、一時停止した場所から再開したりすることはできません。例えば、

function foo() {
    console.log('I');
    console.log('cannot');
    console.log('pause');
}Code language: JavaScript (javascript)

foo()関数は上から下へ実行されます。foo()関数を終了する唯一の方法は、関数から戻るか、エラーをスローすることです。foo()関数を再度呼び出すと、実行は上から下へと開始されます。

foo();

出力

I
cannot
pause

ES6では、通常の関数とは異なる新しい種類の関数、関数ジェネレーターまたはジェネレーターが導入されています。

ジェネレーターは途中で一時停止し、一時停止した場所から再開できます。例えば、

function* generate() {
    console.log('invoked 1st time');
    yield 1;
    console.log('invoked 2nd time');
    yield 2;
}Code language: JavaScript (javascript)

generate()関数を詳しく見てみましょう。

  • まず、functionキーワードの後にアスタリスク(*)があります。アスタリスクは、generate()が通常の関数ではなく、ジェネレーターであることを示します。
  • 次に、yieldステートメントは値を返し、関数の実行を一時停止します。

次のコードは、generate()ジェネレーターを呼び出します。

let gen = generate();Code language: JavaScript (javascript)

generate()ジェネレーターを呼び出すと、

  • まず、コンソールには何も表示されません。generate()が通常の関数であれば、何らかのメッセージが表示されるはずです。
  • 次に、generate()から返り値として何かが返されます。

返り値をコンソールに表示してみましょう。

console.log(gen);Code language: JavaScript (javascript)

出力

Object [Generator] {}Code language: CSS (css)

したがって、ジェネレーターは呼び出されると、その本体を実行せずにGeneratorオブジェクトを返します。

Generatorオブジェクトは、donevalueの2つのプロパティを持つ別のオブジェクトを返します。言い換えれば、Generatorオブジェクトは反復可能です。

以下は、Generatorオブジェクトに対してnext()メソッドを呼び出しています。

let result = gen.next();
console.log(result);Code language: JavaScript (javascript)

出力

invoked 1st time
{ value: 1, done: false }
Code language: CSS (css)

ご覧のとおり、Generatorオブジェクトは、1行目でメッセージ'invoked 1st time'を出力し、2行目で値1を返す(yield)ことで、その本体を実行します。

yieldステートメントは1を返し、2行目でジェネレーターを一時停止します。

同様に、次のコードは、Generatorのnext()メソッドを2回目に呼び出します。

result = gen.next();
console.log(result);
Code language: JavaScript (javascript)

出力

invoked 2nd time
{ value: 2, done: false }Code language: CSS (css)

今回は、Generatorはメッセージ'invoked 2nd time'を出力する3行目から実行を再開し、2を返します(またはyieldします)。

以下は、ジェネレーターオブジェクトのnext()メソッドを3回目に呼び出します。

result = gen.next();
console.log(result);Code language: JavaScript (javascript)

出力

{ value: undefined, done: true }Code language: CSS (css)

ジェネレーターは反復可能であるため、for...ofループを使用できます。

for (const g of gen) {
    console.log(g);
}Code language: JavaScript (javascript)

以下は出力です。

invoked 1st time
1
invoked 2nd time
2

JavaScriptジェネレーターのその他の例

次の例は、ジェネレーターを使用して無限のシーケンスを生成する方法を示しています。

function* forever() {
    let index = 0;
    while (true) {
        yield index++;
    }
}

let f = forever();
console.log(f.next()); // 0
console.log(f.next()); // 1
console.log(f.next()); // 2
Code language: JavaScript (javascript)

foreverジェネレーターのnext()メソッドを呼び出すたびに、0から始まるシーケンスの次の数が返されます。

ジェネレーターを使用してイテレーターを実装する

イテレーターを実装する場合、next()メソッドを手動で定義する必要があります。next()メソッドでは、現在の要素の状態も手動で保存する必要があります。

ジェネレーターは反復可能であるため、イテレーターを実装するためのコードを簡略化できます。

以下は、イテレーターチュートリアルで作成されたSequenceイテレーターです。

class Sequence {
    constructor( start = 0, end = Infinity, interval = 1 ) {
        this.start = start;
        this.end = end;
        this.interval = interval;
    }
    [Symbol.iterator]() {
        let counter = 0;
        let nextIndex = this.start;
        return  {
            next: () => {
                if ( nextIndex < this.end ) {
                    let result = { value: nextIndex,  done: false }
                    nextIndex += this.interval;
                    counter++;
                    return result;
                }
                return { value: counter, done: true };
            }
        }
    }
}
Code language: JavaScript (javascript)

そして、ジェネレーターを使用する新しいSequenceイテレーターがこちらです。

class Sequence {
    constructor( start = 0, end = Infinity, interval = 1 ) {
        this.start = start;
        this.end = end;
        this.interval = interval;
    }
    * [Symbol.iterator]() {
        for( let index = this.start; index <= this.end; index += this.interval ) {
            yield index;
        }
    }
}Code language: JavaScript (javascript)

ご覧のとおり、Symbol.iteratorメソッドは、ジェネレーターを使用することで非常にシンプルになっています。

次のスクリプトは、Sequenceイテレーターを使用して、1から10までの奇数のシーケンスを生成します。

let oddNumbers = new Sequence(1, 10, 2);

for (const num of oddNumbers) {
    console.log(num);
}Code language: JavaScript (javascript)

出力

1
3
5
7
9

ジェネレーターを使用してBagデータ構造を実装する

Bagは、要素を収集し、要素を反復処理できるデータ構造です。アイテムの削除はサポートしていません。

次のスクリプトは、Bagデータ構造を実装しています。

class Bag {
    constructor() {
        this.elements = [];
    }
    isEmpty() {
        return this.elements.length === 0;
    }
    add(element) {
        this.elements.push(element);
    }
    * [Symbol.iterator]() {
        for (let element of this.elements) {
            yield element;
        }
    }
}

let bag = new Bag();

bag.add(1);
bag.add(2);
bag.add(3);

for (let e of bag) {
    console.log(e);
}Code language: JavaScript (javascript)

出力

1
2
3

まとめ

  • ジェネレーターは、ジェネレーター関数function* f(){}によって作成されます。
  • ジェネレーターは、呼び出されたときにすぐにはその本体を実行しません。
  • ジェネレーターは途中で一時停止し、一時停止した場所から実行を再開できます。yieldステートメントは、ジェネレーターの実行を一時停止し、値を返します。
  • ジェネレーターは反復可能であるため、for...ofループで使用できます。
このチュートリアルは役に立ちましたか?