Edited at

JavaScript の ジェネレータ を極める!

More than 1 year has passed since last update.

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 まで実行されます

最後まで関数が実行されると、イテレータリザルトの .donetrue になり、関数の実行が終了します。


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