757
714

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-08-16

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

757
714
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
757
714

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?