【JavaScript】ES2015(ES6)のGenerator関数を試す

 ES2015から実装されたGeneratorという機能について実際に試しながら解説します。

Generator関数とは

 ES2015から実装された、「生成器」の意味を持つ関数です。任意のイテレータを自分で実装することができます。

使い方

 Generatorを実際に使用するには、function宣言の後に*を付与します。

function* generator(){
  // statements
}

 通常のfuncionと違い、値を返す命令は、yieldとなります。

function* generator(){
  yield 1;
}

var gen = generator();  // new ではない

console.log(gen.next().value);  // -> 1
console.log(gen.next().value);  // -> undefined

 generatorは関数です。コンストラクタとして扱ってインスタンスを作成するわけではありませんので注意してください。しかし、ES2016ではコンストラクタが使えるようになるようです。

処理の内容

 通常の感覚だと、Generator関数実行時にイテレータが予め作成され、それを返しているように見えますが、実際はyieldで実行を一時中断しています。

function* generator(){
  var i = 0;
  while(true){
    yield i++;
  }
}

var gen = generator();  // ここで関数が走っているように見えるが……。

gen.next();  // ここで初めてgeneratorの中身が評価される。1回目の呼び出しではgenerator関数のyield句まで実行する。
gen.next();  // 2回目の呼び出しでは、yield句の次行から再開される。
console.log(gen.next().value);  // -> 2

 本例のコードは、通常であればwhile(true)という禁断のコードで無限ループに入り、プログラムがハングアップしてしまうところです。

 しかし、実際にはyieldで関数の実行が中断されているため、何の問題もなく値が取得できることがわかります。

 同じような機能をGeneratorを使わずに実現することもできますが、ここまで簡潔に書くことは難しいでしょう。

使いどころ

 使い方がわかっても、使いどころがわからなければ宝の持ち腐れです。いくつか実際に使えそうな事例を想定してみましょう。

IDの払出に使う

 通常、JavaScript側でIDを払い出すことはそんなにありません。最近はuuidを使うことも増えてきたので、実用性に問題はありますが、試しに実装してみましょう。

const PREFIX = "ID-";

function* idGenerator(){
  var id = 0;
  while(true){
    yield PREFIX + id++;
  }
}

var gen = idGenerator();

function allocateId(){
  return gen.next().value;
}

console.log(allocateId());  // -> ID-0
console.log(allocateId());  // -> ID-1
console.log(allocateId());  // -> ID-2

 Generator関数は引数を受けられるので、払出を途中から始めることもできます。

const PREFIX = "ID-";

function* idGenerator(startId){
  var id = parseInt(startId) || 0;    // 何も渡されなかった場合や無効な数字の場合は0を指定する
  while(true){
    yield PREFIX + id++;
  }
}

var gen = idGenerator(10000);

function allocateId(){
  return gen.next().value;
}

console.log(allocateId());  // -> ID-10000
console.log(allocateId());  // -> ID-10001
console.log(allocateId());  // -> ID-10002

for文の代わりに使う

 通常、ただインクリメントするだけのiですが、こんなこともできます。

function* incrementGenerator(start, end, step){
  var i = parseInt(start) || 0;
  end = parseInt(end) || 0;
  step = parseInt(step) || 1;
  for(; i < end; i+=step){
    yield i;
  }
}
var gen = incrementGenerator(10, 100, 2);
var next = gen.next();

while(!next.done){
  console.log(next.value);
  next = gen.next();
}

 next()で順当に書こうとすると逆に複雑化しましたが、for~of文も使うことができます。

function* incrementGenerator(start, end, step){
  var i = parseInt(start) || 0;
  end = parseInt(end) || 0;
  step = parseInt(step) || 1;
  for(; i < end; i+=step){
    yield i;
  }
}
var gen = incrementGenerator(10, 100, 2);

for(var value of gen){
  console.log(value);
}

 こちらは非常に簡潔ですね。

フィボナッチ数列のGenerator

 フィボナッチ数列をGenerator関数で実装してみます。

function* fibonacciGenerator(){
  var before = 0;
  yield before;  // まず最初の0を返す
  var after = 1;
  yield after;  // 次の1を返す
  while(true){
    var current = before + after;
    before = after;
    after = current;
    yield current;  // フィボナッチ数を返す
  }
}

var gen = fibonacciGenerator();

var arrayFibonacci = [];
for(var i = 0; i < 10; i++){
  arrayFibonacci.push(gen.next().value);
}

console.log(arrayFibonacci);

 目的の添字を指定してフィボナッチ数を抜き出してくることはできませんが、必要なときにだけ計算が行われるため、パフォーマンス上の利点があります。

 他、ロジックを関数に隠蔽できるので、可読性面でも非常に優れた書き方になります。

まとめ

 ES2015は、そろそろモダンブラウザの対応もほぼ終わり、Babelを使わずとも実用レベルに達してきました。今後はこういった構文も増えてくると思いますので、戸惑わないように、また適切に使えるように引き出しを増やしておきましょう。