Understanding Generators in JavaScript

The author selected Open Internet/Free Speech Fund as part of Write for DOnations program.

Introduction

ECMAScript 2015 で、ジェネレーターが JavaScript 言語に導入されました。 ジェネレータとは、一時停止と再開が可能で、複数の値を得ることができる処理のことです。 JavaScriptにおけるジェネレータは、反復可能なGeneratorオブジェクトを返すジェネレータ関数で構成されています。

ジェネレーターは状態を保持できるため、イテレータを効率的に作成できます。また、無限データストリームを扱うことができるため、Web アプリケーションのフロントエンドで無限スクロールを実装したり、音波データを操作したりするのに使用できます。 さらに、プロミスと共に使用すると、ジェネレータはasync/awaitの機能を模倣することができ、より分かりやすく読みやすい方法で非同期なコードを扱うことができます。 async/await は API からデータを取得するような一般的で単純な非同期ユースケースを扱うより一般的な方法ですが、ジェネレーターには、その使用方法を学ぶ価値のあるより高度な機能があります。

この記事では、ジェネレーター関数の作成方法、Generator オブジェクトに対する反復処理方法、ジェネレーター内の yieldreturn との相違、その他ジェネレーターで作業を行う上での側面を説明します。

ジェネレーター関数

ジェネレーター関数とは、Generatorオブジェクトを返す関数で、次のようにfunctionキーワードにアスタリスク(*)を付けて定義します:

// Generator function declarationfunction* generatorFunction() {}

時折、function *generatorFunction()などの関数キーワードに対して、関数名の横にアスタリスクが表示されることがあります。 これは同じように機能しますが、function* はより広く受け入れられている構文です。

ジェネレーター関数は、正規関数と同様に式で定義することもできます。

Note: 通常の関数とは異なり、ジェネレーターは new キーワードを使用して構築することはできませんし、矢印関数と組み合わせて使用することもできません。

ジェネレータ オブジェクト

JavaScript では伝統的に、関数は完了するまで実行され、関数を呼び出すと、return キーワードに到達したときに値が返されます。 もしreturnキーワードが省略された場合、関数は暗黙のうちにundefinedを返します。

例えば次のコードでは、2つの整数の引数の合計である値を返す sum() 関数を宣言します:

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

関数を呼び出すと、引数の合計である値

const value = sum(5, 6) // 11

しかし、ジェネレータ関数はすぐに値を返さず、反復可能 Generator オブジェクトを返します。

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

ジェネレータ関数を呼び出すと、Generator オブジェクトが返され、それを変数に代入することができます。 しかし、実際に得られるのは suspended 状態のオブジェクトです。

この関数が返す Generator オブジェクトはイテレータです。 イテレータとは、一連の値を反復処理するために使用されるnext()メソッドが利用可能なオブジェクトのことです。 next()メソッドはvaluedoneのプロパティを持つオブジェクトを返す。 valueは返された値を表し、doneはイテレータがすべての値を実行したかどうかを示している。

これがわかった上で、generator に対して next() を呼び出し、イテレータの現在の値と状態を取得しましょう。 イテレータが終了したので、ジェネレータ関数の状態はsuspendedからclosedに変化する。 generator を再度呼び出すと次のようになります:

Output
generatorFunction {<closed>}

今のところ、ジェネレータ関数が関数の return 値を得るより複雑な方法であることを示しただけです。 しかし、ジェネレータ関数にも通常の関数と異なるユニークな特徴があります。 次のセクションでは、yield 演算子について学び、ジェネレーターがどのように実行を一時停止および再開できるかを見ていきます。

yield 演算子

ジェネレーターは JavaScript に新しいキーワードを導入しています。 yield. yield はジェネレーター関数を一時停止し、yield に続く値を返すことができ、値を通して反復する軽量な方法を提供します。

この例では、異なる値でジェネレーター関数を 3 回一時停止し、最後に値を返します。

さて、ジェネレータ関数で next() を呼び出すと、yield に遭遇するたびに一時停止します。 doneyield が出るたびに false にセットされ、ジェネレータがまだ終了していないことを表しています。 returnになるか、yieldがなくなるとdonetrueに反転し、ジェネレータは終了します。

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

この場合、次の4行が順番に出力されます。

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

ジェネレータは return を必要としないことに注意してください。

Iterating Over a Generator

next() メソッドを使用して、フル オブジェクトのすべての value および done プロパティを受け取り、Generator オブジェクトを手動で反復処理しました。 しかし、ArrayMapSet と同様に、Generator も反復処理プロトコルに従っており、for...of で反復処理することができます:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

この場合、次のようになります。

// Create an array from the values of a Generator objectconst values = console.log(values)

これは次のような配列になります:

Output
(3)

spread と for...of は両方とも return を値に織り込みません(この場合、'The Oracle' となるはずです)。

注意: これらのメソッドはどちらも有限のジェネレーターを扱うのに効果的ですが、ジェネレーターが無限のデータ ストリームを扱っている場合、無限ループを作成せずに spread または for...of を直接使用することはできません。

Closing a Generator

これまで見てきたように、すべての値を反復することによりジェネレーターはその done 属性を true へ、その状態を closed へセットさせることが可能です。 return() メソッドと throw() メソッドです。

return() では、return 文が関数本体にあったかのように、ジェネレータを任意の時点で終了させることができます。

return() のデモンストレーションとして、関数定義に return がなく、いくつかの yield 値を持つジェネレータを作成します。 その直後にGeneratorオブジェクトに対してreturn()メソッドを呼び出すと、今度は渡された値が得られ、doneにはtrueがセットされる。 next() への追加の呼び出しは、未定義の値を持つデフォルトの完了したジェネレータ応答を与えます。

これを実証するために、generator で次の 3 つのメソッドを実行します:

generator.next()generator.return('There is no spoon!')generator.next()

この結果、次の 3 つが得られます:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

return()メソッドは Generator オブジェクトに完了を強い、他の yield キーワードは無視するようにしました。 これは、ユーザーが別のアクションを実行したいときに Web リクエストを中断するなど、関数をキャンセル可能にする必要がある非同期プログラミングで特に有用です。

ジェネレーター関数の本体にエラーをキャッチして処理する方法がある場合、throw()メソッドを使用してジェネレーターにエラーをスローすることが可能です。

これを実証するために、ジェネレーター関数本体の中にtry...catchを置き、エラーが見つかったらログに記録します。

次に、next()メソッドを実行し、throw()を実行します。

generator.next()generator.throw(new Error('Agent Smith!'))

これにより、次の出力が得られます。

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

throw() を使用して、ジェネレーターにエラーを注入し、それを try...catch がキャッチしてコンソールにログ記録しました。

ジェネレーター オブジェクトのメソッドと状態

Generator オブジェクトで使用できるメソッドの一覧は次の表のとおりです。

Method Description
next() Return the next value in a generator
return() Return a value in the game of generator ジェネレータを終了する
throw() エラーをスローしてジェネレータを終了する

次の表はGeneratorオブジェクトの取り得るステートをリストアップしている。

Generator has finished by either encountered an error.Generator has replaced in the extension, return, or iterating through all values

Status Description
suspended Generator has halted execution but not terminated
closed

yield Delegation

通常のyield演算子に加え、ジェネレータはyield*式を使って、さらに別のジェネレータに値を委譲することができます。 ジェネレータ内で yield* が見つかると、委任されたジェネレータの中に入り、そのジェネレータが閉じられるまですべての yield を繰り返し始めます。 これは、異なるジェネレータ関数を分離してコードを意味的に整理するために使用でき、同時にすべての yield を正しい順序で反復させることができます。

では、2 つのジェネレーター関数を作成し、一方が他方に対して yield* 操作することを説明します。

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

これで次の値が生成順に得られます。

Output
1234

外部ジェネレータは値 12 を生成し、yield* で他のジェネレータに委ね、34 が返ってきました。

yield* は Array や Map などの反復可能なオブジェクトに委譲することもできる。 ジェネレーター内で yield を使用したい任意の関数もジェネレーターでなければならないため、降伏の委譲はコードの整理に役立ちます。 これは、数を 1 つずつ増加させるジェネレーター関数内で無限ループを作成することによって実証できます。

以下のコード ブロックでは、このジェネレーター関数を定義し、ジェネレーターを開始します。

では、next() を使用して値を繰り返し処理します。

ジェネレータでは、実行を任意に停止・再開できるため、無限ループを作る心配はない。 しかし、ジェネレータをどのように呼び出すかについては、まだ注意を払う必要があります。 無限データ ストリームで spread または for...of を使用すると、無限ループを一度に反復することになり、環境がクラッシュします。

より複雑な無限データ ストリームの例として、フィボナッチ ジェネレーター関数を作成できます。 これをテストするために、有限の数でループし、フィボナッチ数列をコンソールに表示します。 これは、Web アプリケーションのフロントエンドで無限スクロールを実装するような例に役立ちます。

Generators で値を渡す

この記事全体を通して、私たちはジェネレーターをイテレーターとして使用し、各反復で値を生成しています。 値を生成するだけでなく、ジェネレータは next() から値を消費することもできます。 この場合、yield には値が含まれます。

ここで重要なのは、最初に呼び出される next() は値を渡さず、ジェネレータを起動するだけであるということです。

これにより、次の出力が得られます:

Output
100200{value: "The end", done: true}

また、ジェネレータに初期値を設定することも可能です。 次の例では、forループを作成し、各値をnext()メソッドに渡しますが、初期関数にも引数を渡します。

next()から値を取り出し、次の反復に新しい値(前の値を10倍した値)を渡します。 これは次のようになります:

Output
010203040

ジェネレーターの起動に対処する別の方法は、他の何かを行う前に常に next() を一度呼び出す関数でジェネレーターをラップすることです。

async/await with Generators

A asynchronous function is a type of function available in ES6+ JavaScript that makes working with asynchronous data appear it synchronous by easier to understandable as an synchronous. ジェネレータは、非同期関数よりも豊富な機能を持ちますが、同様の動作を再現することが可能です。 この方法で非同期プログラミングを実装すると、コードの柔軟性を高めることができます。

このセクションでは、ジェネレーターを使用して async/await を再現する例を示します。

JSONPlaceholder API (テスト用に JSON データの例を提供) から Fetch API でデータを取得して、応答をコンソールにログ記録する非同期関数を構築してみましょう。

最初に、API からデータをフェッチしてオブジェクトの配列を返す getUsers という非同期関数を定義し、次に getUsers を呼び出します:

これにより、次のような JSON データが生成されます。 その代わり、私たちが作成した新しい関数と、await 約束の代わりに yield 値を使用します。

次のコード ブロックでは、getUsers という関数を定義し、新しい asyncAlt 関数を使用して async/await を模倣します (これは後で作成します)。

見てのとおり、値を生成するジェネレーター関数が渡されることを除けば、async/await の実装とほとんど同じに見えます。

では、非同期関数に似た asyncAlt 関数を作成します。 asyncAlt はパラメータとしてジェネレータ関数を持ち、これは fetch が返す約束を生成する私たちの関数です。 asyncAlt は関数自体を返し、見つけたすべてのプロミスを最後の 1 つまで解決します:

これは async/await バージョンと同じ出力になります:

この実装は async/await に代えてジェネレーターを使用する方法のデモであり、すぐに製品化できる設計ではないことに注意しましょう。 また、エラー処理も設定されていませんし、生成された値にパラメータを渡す機能もありません。 このメソッドはコードに柔軟性を追加できますが、多くの場合、async/await は実装の詳細を抽象化し、生産的なコードを書くことに集中できるため、より良い選択となるでしょう。 一般的には使用されませんが、JavaScript の強力で多用途な機能です。 このチュートリアルでは、ジェネレーター関数とジェネレーターオブジェクト、ジェネレーターで利用できるメソッド、yieldyield* 演算子、有限および無限データセットで使用されるジェネレーターについて学習しました。 また、ネストされたコールバックや長いプロミス チェーンなしで非同期コードを実装する 1 つの方法を探りました。

JavaScript の構文についてさらに学習したい場合は、「JavaScript における This, Bind, Call, and Apply の理解」および「JavaScript における Map および Set オブジェクトの理解」チュートリアルを参照してください。

コメントを残す

メールアドレスが公開されることはありません。