JavaScript の ジェネレータ を極める!
(https://qiita.com/kura07/items/d1a57ea64ef5c3de8528)

 この記事は最終更新日から1年以上が経過しています。

ECMAScript 6(2015年6月に公開され、今もなお比較的新しい JavaScript)の大目玉である イテレータ と ジェネレータ。なかなかに複雑で巨大な仕組みになっていてややこしいです。
そこで今回は ジェネレータ を、順を追って理解できるように解説したいと思います。

また、実用的なサンプルを「3. 実用サンプル」に示しました。
初めにこちらを見て、何ができるのかを知ってから読み始めるのもオススメです。

(2017年3月現在、オープンなページでの使用はまだ避けたほうがいいかもしれませんが、実装は確実に進んでいます。ECMAScript 6 compatibility table

1. ジェネレータ、ジェネレータ関数 とは

ジェネレータ は、イテレータ を強力にサポート するものです。

例えば、1~20の数を順番に取り出す for-of文 は、以下のように書くことができます。
ジェネレータ は使っていません。)

1~20の数を順番に取り出すfor-of文
var ary = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
for(var num of ary) console.log(num);
/*
  1
  2
  3
  ...
  20
*/

この書き方でも十分に分かりやすいです。
しかし、取り出す数が1ずつ増えていくという処理を、関数でスマートに書きたいものです。
そこで、ジェネレータ を使えばもっとスマートに書くことができます

ジェネレータを使って1~20の数を順番に取り出すfor-of文
function* gfn(from, to){ while(from <= to) yield from++; }
var g = gfn(1, 20);
for(var num of g) console.log(num);
/*
  1
  2
  3
  ...
  20
*/

このような簡単な例だけでなく、2倍ずつにして順番に取り出したり、フィボナッチ数列を順番に取り出したりすることもできます。
このように、ジェネレータ は イテレータ を強力にサポートすることができるのです。

ここにおいて、

  • ジェネレータ関数 は、function* gfn(from, to){ while(from <= to) yield from++; }
  • ジェネレータ は、gfn(1, 20) のように ジェネレータ関数 から得ることのできるオブジェクト

を、それぞれ指す用語です。

ジェネレータ は、イテラブル であり、なおかつ イテレータ でもあります。
つまり、Qiita: JavaScript の イテレータ を極める! の 2.6.2. イテラブルなオブジェクト の利用法 で紹介したような利用法ができます。

2. ジェネレータ を使う

2.1. ジェネレータ関数 の書き方、使い方

便利な ジェネレータ関数 の書き方を学びましょう。といっても、普通の関数の書き方とほとんど違いはありません。
普通の関数と違う点は、以下のたった2点だけです。

  • ジェネレータ関数 は、function* gfn(){} または var gfn = function*(){}; のように、functionのあとに * を記述する必要がある
  • ジェネレータ関数 では、yield 及び yield* を使うことができる

例として簡単な ジェネレータ関数 を見てみましょう。

ジェネレータ関数
function* gfn(){
    var a = yield 0;
    yield* [1, a, 5];
}

これで ジェネレータ関数は完成しました。
ジェネレータ関数 から ジェネレータ を作るには、単に gfn() のように記述すればオッケーです。
ただし、gfn() の時点では関数の中身が実行されないことは要注意です
関数の中身は、gfn() で生成された ジェネレータ から .next() で値を取り出す時点で実行されます。

ジェネレータ関数からジェネレータを作って実行する
function* gfn(){
    var a = yield 0;
    yield* [1, a, 5];
}

var g = gfn(); // ジェネレータを作った。この時点ではまだ関数の中身は実行されない

// g.next() を実行すると、関数の中身が順番に実行される
console.log( g.next() ); //  { value: 0, done: false }
console.log( g.next(3) ); // { value: 1, done: false }
console.log( g.next() ); //  { value: 3, done: false }
console.log( g.next() ); //  { value: 5, done: false }
console.log( g.next() ); //  { value: undefined, done: true }

それでは、もっと簡単なコードを例にして、ジェネレータ関数 の仕組みを見てみましょう。

2.2. yield

yield式
function* gfn(n){
    n++;
    yield n;
    n *= 2;
    yield n;
    n = 0;
    yield n;
}

var g = gfn(10); // ジェネレータを作った

console.log( g.next() ); // { value: 11, done: false }
// n++; が実行された後、yield n; によって n の値が返された。

ジェネレータ を作って .next() を実行すると、最初の yield が出てくるまで関数が実行されます
yield まで関数が実行されると、関数の実行はいったん停止し、イテレータリザルトとして値が返されます

再び .next() を実行すると、いったん停止した位置から再び関数が再開され、次の yield まで実行されます
最後まで関数が実行されると、イテレータリザルトの .done が true になり、関数の実行が終了します。

yield式
function* gfn(n){
    n++;
    yield n;
    n *= 2;
    yield n;
    n = 0;
    yield n;
}

var g = gfn(10); // ジェネレータを作った

console.log( g.next() ); // { value: 11, done: false }
// n++; が実行された後、yield n; によって n の値が返された。

console.log( g.next() ); // { value: 22, done: false }
// n *= 2; が実行された後、yield n; によって n の値が返された。

console.log( g.next() ); // { value: 0, done: false }
// n = 0; が実行された後、yield n; によって n の値が返された。

console.log( g.next() ); // { value: undefined, done: true }
// 関数の実行が終了したので、.done が true になった。

2.3. ジェネレータに値を渡す

.next(val) のように値を渡してやることで、ジェネレータに値を渡すことができます

ジェネレータに値を渡す
function* gfn(){
    var a = yield "first";
    var b = yield "second";
    yield a + b;
}

var g = gfn();

console.log( g.next() ); // { value: "first", done: false }

console.log( g.next(3) ); // { value: "second", done: false }
// yield "first" の部分が 3 に置き換えられる

console.log( g.next(5) ); // { value: 8, done: false }
// yield "second" の部分が 5 に置き換えられる

console.log( g.next() ); // { value: undefined, done: true }

g.next(3) などを実行することで、ジェネレータ関数の中身に値を渡しています。
渡した値は、直前にいったん停止した yield と置き換えられたように渡されます。

2.4. yield*

yield のほかに yield* という便利な式があります。
yield* には イテラブルなオブジェクト を与えます。
すると、イテラブルなオブジェクト から順番に値を取り出し、それぞれの値に対して yield を行ってくれます

yield*式
function* gfn(){
    yield* [1, 3, 5];
}

var g = gfn();

console.log( g.next() ); // { value: 1, done: false }
console.log( g.next() ); // { value: 3, done: false }
console.log( g.next() ); // { value: 5, done: false }
console.log( g.next() ); // { value: undefined, done: true }
yield*式
function* gfn(){
    yield* "ひよこ";
}

var g = gfn();

console.log( g.next() ); // { value: "ひ", done: false }
console.log( g.next() ); // { value: "よ", done: false }
console.log( g.next() ); // { value: "こ", done: false }
console.log( g.next() ); // { value: undefined, done: true }

つまり、for(var v of iterable) yield v; と同様の処理を行っているというわけです。

2.5. 簡単なサンプル

全て、ジェネレータが イテラブルなオブジェクト であることを利用したサンプルです。

ジェネレータを利用する
function* gfn(){
    yield 1;
    yield* [2, 1, 2];
}

for(var num of gfn()) console.log(num);
/*
  1
  2
  1
  2
*/

console.log( [...gfn()] ); // [1, 2, 1, 2]

console.log( Math.max(...gfn()) ); // 2

var [a, b, c, d] = gfn();
console.log(a, b, c, d); // 1, 2, 1, 2

console.log( new Set(gfn()) ); // Set {1, 2}

2.6. ジェネレータ のもう一つの利用法

今まで見てきた ジェネレータ は、イテレータ として利用することに重点を置いてきました。

しかし、見方を変えれば、ジェネレータ はもう一つの使い方ができます。
それは、自由に途中でいったん停止できる関数 という見方です。

ページをクリックするたびにひとつずつアラートを出す
function* gfn(){
    alert("こんにちは!"); yield;
    alert("良い天気ですね。"); yield;
    alert("さようなら!");
}
var g = gfn();
document.onclick = function(){ g.next(); }; // ページをクリックするたびに g.next(); を実行する

コード中の yield の時点で、関数の実行をいったん停止することができる と考えると、分かりやすいかと思います。
これは、非同期処理をする際に、かなりの力を発揮します。

3. 実用サンプル

ジェネレータ で説明することは以上ですが、実際のサンプルがないと、どのようにつかえるかのイメージがつきにくいと思います。
いくつかサンプルを挙げますので、参考にしてください。

1000以下のフィボナッチ数を列挙する
function* fibonacci(){
    var a = 0, b = 1, temp;
    while(true){
        temp = a + b;
        a = b; b = temp;
        yield a;
    }
}

var g = fibonacci();
for(var num of g){
    if(num > 1000) break;
    console.log(num);
}
/*
  1
  2
  3
  5
  8
  13
  21
  34
  55
  89
  144
  233
  377
  610
  987
*/
ランダムな自然数の配列を作る
function* randomIntArray(max, len){
    for(var i=0;i<len;i++) yield Math.floor(Math.random() * max) + 1;
}

console.log( [...randomIntArray(2, 10)] ); // 例:[1, 2, 1, 1, 1, 2, 2, 2, 1, 2]
console.log( [...randomIntArray(6, 4)] ); // 例:[1, 6, 2, 4]
組み合わせを順番に取り出す
function* combination(ary, len){
    yield* (function* gfn(a, ary){
        if(a.length < len){
            for(var i=0;i<ary.length-len+a.length+1;i++){
                yield* gfn(a.concat(ary[i]), ary.slice(i+1));
            }
        }
        else yield a;
    })([], ary);
}

for(v of combination([1,2,3], 2)) console.log(v);
/*
  [1, 2]
  [1, 3]
  [2, 3]
*/

for(v of combination(["A", "B", "C", "D", "E"], 3)) console.log(v.join(""));
/*
  ABC
  ABD
  ABE
  ACD
  ACE
  ADE
  BCD
  BCE
  BDE
  CDE
*/
非同期処理を分かりやすく書く
function easyAsync(gfn){
    var g = gfn();
    (function ok(value){
        var result = g.next(value);
        if(!result.done) result.value(ok);
    })();
}

easyAsync(function*(){
    alert("こんにちは!");
    yield function(ok){
        setTimeout(ok, 3000);
    };
    alert("3秒たちました!");
    yield function(ok){
        document.onclick = ok;
    };
    document.onclick = null;
    alert("ページをクリックしました!");
    var sourse = yield function(ok){
        var xhr = new XMLHttpRequest();
        xhr.open("GET", location.href, true);
        xhr.send(null);
        xhr.onload = function(){ ok(xhr.responseText); };
    };
    alert("このページのソースは" + sourse.length + "文字です!");
});

4. 参考

ECMAScript 2015 Language Specification – ECMA-262 6th Edition
Iterators and generators - JavaScript | MDN
ジェネレータについて - JS.next
ECMAScript 6 compatibility table


+ Recent posts