ECMAScript 6(2015年6月に公開され、今もなお比較的新しい JavaScript)の大目玉である イテレータ と ジェネレータ。なかなかに複雑で巨大な仕組みになっていてややこしいです。
そこで今回は ジェネレータ を、順を追って理解できるように解説したいと思います。
- Qiita: JavaScript の イテレータ を極める! ←こちらから読むのがオススメです
- Qiita: JavaScript の ジェネレータ を極める!(この記事)
また、実用的なサンプルを「3. 実用サンプル」に示しました。
初めにこちらを見て、何ができるのかを知ってから読み始めるのもオススメです。
(2017年3月現在、オープンなページでの使用はまだ避けたほうがいいかもしれませんが、実装は確実に進んでいます。ECMAScript 6 compatibility table)
1. ジェネレータ、ジェネレータ関数 とは
ジェネレータ は、イテレータ を強力にサポート するものです。
例えば、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ずつ増えていくという処理を、関数でスマートに書きたいものです。
そこで、ジェネレータ を使えばもっとスマートに書くことができます。
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
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
になり、関数の実行が終了します。
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
を行ってくれます。
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 }
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. 実用サンプル
ジェネレータ で説明することは以上ですが、実際のサンプルがないと、どのようにつかえるかのイメージがつきにくいと思います。
いくつかサンプルを挙げますので、参考にしてください。
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