星つきのメソッド?JavaScriptのGeneratorについて

はじめに

普段はあまり見かけないですがたまにオープンソースのJavaScriptコードを読んでいると、以下のように*がついているものを見かけることがあります。

function *gen() {
  yield 1;
  yield 2;
  yield 3;
}

しかし、JSでは変数名に$_以外の特殊文字は使用できません。 これは何をするものなのでしょう?

Generatorとは

直訳すると「生成機」になるGeneratorは、メソッドを 途中で止めることができる という特性を持っています。 一般的にJSでのメソッドは一度実行されるとreturnするまでにはすべての処理が動きます。

しかし、Generatorの場合はメソッド内のyieldがある時にメソッドがその時点で止まります。

動き方

概念だけではわかりにくいため、実際の動作を見ていきたいと思います。

上記でもあったgeneratorメソッドがあったとした時

function *gen() {
  yield 1;
  yield 2;
  yield 3;
}

以下のようにnext()を通してどんどん値を変えていくことができます。

const generator = gen();

generator.next(); // { value: 1, done: false }
generator.next(); // { value: 2, done: false }
generator.next(); // { value: 3, done: false }
// 呼び出せるものがなくなったらdoneがtrueになる
generator.next(); // { value: undefined, done: true }

実用例

こういうものがあるのはわかったものの、どこに使えばいいの?と思われる方がいらっしゃるかもしれませんね。 頻繁に使うようなものではありませんが、実はgenerator、いろいろなことを便利にすることができます。

無限を表現できる

Generatorの強いところは、無限に処理をを扱えることです。 例えば、以下のように無限のフィボナッチ数列を生成するGeneratorを書くことができます。

function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3

このコードは無限ループを含んでいますが、yieldで実行が一時停止させることができるため必要な分だけ値を取り出すことができます。 メモリ内にすべての値を保持する必要がないのでパフォーマンスにもいいです。

イテレータとして使用する

最近のES2025ではIteratorの機能がいっぱい増えましたね!

qiita.com

そのIteratorの機能を使っていくためにもGeneratorはいい媒体になります。

function* arrayIterator(array) {
  for (let i = 0; i < array.length; i++) {
    yield array[i];
  }
}

const myArray = [1, 2, 3, 4, 5];
const iterator = arrayIterator(myArray);

// for...ofループで使用可能
for (const item of iterator) {
  console.log(item); // 1, 2, 3, 4, 5 が順番に出力される
}

// スプレッド演算子でも使用可能
const newArray = [...arrayIterator(myArray)];
console.log(newArray); // [1, 2, 3, 4, 5]

応用編

next以外にもthrow, returnも使える

throwが使えるため、以下のようにtry catchでエラーをハンドリングするときにも応用できます。

function* errorHandlingGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (e) {
    console.log('エラーをキャッチしました:', e);
    yield 'エラー後';
  } finally {
    console.log('クリーンアップ処理');
  }
}

const gen = errorHandlingGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw('カスタムエラー')); // エラーをキャッチしました: カスタムエラー
                                       // { value: 'エラー後', done: false }
console.log(gen.return('終了')); // クリーンアップ処理
                               // { value: '終了', done: true }

yieldを変数に入れることができる

変数に入れることで、新しい値を入れてあげることができます。 next()メソッドから渡したものをもとにGeneratorの中で使うことができます

function* twoWayCommunication() {
  const a = yield 'First value?';
  console.log('Received:', a);
  
  const b = yield 'Second value?';
  console.log('Received:', b);
  
  return 'All done!';
}

const comm = twoWayCommunication();
console.log(comm.next()); // { value: 'First value?', done: false }
console.log(comm.next('Hello')); // Received: Hello
                                // { value: 'Second value?', done: false }
console.log(comm.next('World')); // Received: World
                                // { value: 'All done!', done: true }

ちょっとややこしい仕様ですが、初めてのnext()に入れた値は捨てられるため注意が必要です。

上記のように変数に値を入れる時、まずは一度next()を実行した後、二つ目のnext('Hello')aに入っていくようなことをする必要があります。

おわりに

私たちインゲージではこのように新しいECMAの機能や面白いJavaScriptの機能を積極的に使っています。 共にJavaScriptの海に飛び込みたい!という方がいらっしゃいましたらぜひ弊社の採用ページにてエントリーお願いします!