ES6 Generator 関数の構文と非同期制御の仕組み

Generator 関数の概要

ES6 で導入された Generator 関数は、実行を任意の時点で中断し、後から再開できる特殊な関数です。従来の関数とは異なり、内部状態を保持しながら段階的に処理を進めることができるため、非同期処理や複雑な制御フローの実装に適しています。

Generator 関数は以下の観点から理解できます。

  • 状態管理: 内部状態をカプセル化したステートマシンとして動作します。
  • 構文特徴: 関数宣言にアスタリスク(*)を含み、本体内で yield 式を使用して状態の分岐点を定義します。
  • 戻り値: 関数呼び出し直後は実行されず、内部状態を指すイテレータオブジェクトを返します。
  • 実行制御: yield で中断し、next() メソッドで再開します。

基本構文と実行フロー

関数名の前に * を付与することで Generator 関数を定義します。アスタリスクの位置は柔軟ですが、標準的には function* とします。

function* createSequence() {
  yield 10;
  yield 20;
  return 30;
}

const iterator = createSequence();

console.log(iterator.next()); 
// { value: 10, done: false }

console.log(iterator.next()); 
// { value: 20, done: false }

console.log(iterator.next()); 
// { value: 30, done: true }

console.log(iterator.next()); 
// { value: undefined, done: true }

上記の例では、next() を呼び出すたびに処理が進行し、yield 式の値が value プロパティとして返されます。done プロパティは、処理が完了したかどうかを示す布尔値です。

yield 式の動作

yield は関数の実行を一時停止する命令です。next() が呼ばれるまで、その後のコードは評価されません。これにより、必要な時点でのみ計算を行う遅延評価が可能になります。

yieldreturn の違いは以下の通りです。

  • yield は実行を中断し、複数回使用可能です。
  • return は関数を終了させ、値を返します。

注意点として、yield 式は Generator 関数内でのみ使用可能です。また、式の中で使用する際は括弧で囲む必要があります。

function* demo() {
  // 構文エラー
  // console.log('Result: ' + yield); 
  
  // 正しい書き方
  console.log('Result: ' + (yield)); 
}

Iterator インターフェースとの関係

Generator 関数はイテレータを生成するため、オブジェクトの Symbol.iterator に割り当てることで、そのオブジェクトを_iterable_にできます。

const collection = {};
collection[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

console.log([...collection]); // [1, 2, 3]

生成されたイテレータオブジェクト自体もイテレータインターフェースを実装しており、自身を返します。

next メソッドへの引数渡し

next() メソッドに引数を渡すと、その値は直前の yield 式の戻り値として扱われます。これにより、実行中に外部から値を注入できます。

function* calculator(input) {
  const step1 = yield (input * 2);
  const step2 = yield (step1 / 4);
  return input + step1 + step2;
}

const proc = calculator(5);
proc.next();       // 開始
proc.next(12);     // step1 に 12 を代入
proc.next(13);     // step2 に 13 を代入

最初の next() は関数起動用であり、引数は無視されます。2 回目以降の引数が有効になります。

for...of ループとの連携

for...of ループは自動的にイテレータの next() を呼び出し、donetrue になるまで繰り返します。

function* numbers() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

for (const n of numbers()) {
  console.log(n); // 1, 2, 3 が出力される(4 は含まれない)
}

spread 演算子や配列分解代入も同様にイテレータインターフェースを利用するため、Generator と相性が良いです。

エラー処理:throw メソッド

イテレータオブジェクトの throw() メソッドを使用すると、Generator 関数内部にエラーを投げることができます。内部の try...catch で捕获可能です。

function* safeProcess() {
  try {
    yield 1;
  } catch (e) {
    console.log('Caught inside:', e);
  }
}

const it = safeProcess();
it.next();
it.throw('Error Occurred'); // 内部で捕获される

内部で捕获されない場合、エラーは外部の try...catch に伝播します。エラーが発生すると、その Generator の実行は終了したとみなされます。

処理の終了:return メソッド

return() メソッドを呼び出すと、Generator 処理を強制的に終了し、指定された値を返します。

function* sequence() {
  yield 1;
  yield 2;
}

const gen = sequence();
gen.next(); 
gen.return('Finished'); // { value: 'Finished', done: true }

内部に finally ブロックがある場合、それが実行されてから終了します。

yield* 式による委譲

yield* を使用すると、別の Generator 関数やイテレータ可能なオブジェクトに処理を委譲できます。

function* sub() {
  yield 'a';
  yield 'b';
}

function* main() {
  yield 'start';
  yield* sub();
  yield 'end';
}

// 'start', 'a', 'b', 'end' の順に出力
for (const v of main()) {
  console.log(v);
}

これはネストされた構造を平坦化する場合にも有用です。

function* flatten(array) {
  for (const item of array) {
    if (Array.isArray(item)) {
      yield* flatten(item);
    } else {
      yield item;
    }
  }
}

this のバインディング

Generator 関数はコンストラクタとして使用できません(new は不可)。返回されるイテレータが関数のインスタンスとなります。

function* Gen() {
  this.value = 10;
}
// const g = new Gen(); // エラー

const obj = {};
const iter = Gen.call(obj);
// obj.value にアクセス可能

コンテキストを維持したい場合は、callapply で明示的に this を绑定する必要があります。

ステートマシンとコルーチン

Generator 関数は内部状態を保持するため、ステートマシンの実装に適しています。また、複数の処理単位が実行権を譲り合う「コルーチン」の概念も実現できます。

従来のサブルーチンは呼び出し元に戻るまで終了しませんが、コルーチンは実行を中断し、別の処理に権限を渡せます。JavaScript は単线程ですが、Generator を使うことで複数のタスクが交互に実行されるような振る舞いを作れます。

実用パターン

Generator の中断・再開機能は、非同期処理を同期的なコードのように記述する際に役立ちます。

function* asyncTask() {
  showLoading();
  const data = yield fetchData();
  hideLoading();
  return data;
}

また、独自のイテレータインターフェースをオブジェクトに実装する際にも利用できます。

function* objectEntries(obj) {
  const keys = Object.keys(obj);
  for (const key of keys) {
    yield [key, obj[key]];
  }
}

const data = { x: 1, y: 2 };
for (const [k, v] of objectEntries(data)) {
  console.log(k, v);
}

タグ: javascript ES6 Generator Iterator AsynchronousProgramming

8月2日 11:49 投稿