JavaScriptのジェネレーターを理解する
著者は、 Open Internet / Free Speech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
ECMAScript 2015 では、ジェネレーターがJavaScript言語に導入されました。 ジェネレーターは、一時停止および再開できるプロセスであり、複数の値を生成できます。 JavaScriptのジェネレーターは、ジェネレーター関数で構成され、反復可能なGeneratorオブジェクトを返します。
ジェネレーターは状態を維持し、イテレーターを効率的に作成する方法を提供し、Webアプリケーションのフロントエンドに無限スクロールを実装したり、音波データを操作したりするために使用できる無限データストリームを処理できます。 さらに、 Promises と併用すると、ジェネレーターはasync/await
機能を模倣できるため、非同期コードをより簡単で読みやすい方法で処理できます。 async/await
は、APIからデータをフェッチするなど、一般的で単純な非同期のユースケースを処理するためのより一般的な方法ですが、ジェネレーターには、それらの使用方法を学ぶ価値のあるより高度な機能があります。
この記事では、ジェネレーター関数を作成する方法、Generator
オブジェクトを反復処理する方法、ジェネレーター内のyield
とreturn
の違い、およびその他の側面について説明します。ジェネレーターの操作。
ジェネレーター機能
ジェネレーター関数は、Generator
オブジェクトを返す関数であり、function
キーワードとそれに続くアスタリスク(*
)で定義されます。以下では:
// Generator function declaration function* generatorFunction() {}
function *generatorFunction()
などの関数キーワードではなく、関数名の横にアスタリスクが表示される場合があります。 これは同じように機能しますが、function*
はより広く受け入れられている構文です。
ジェネレーター関数は、通常の関数のように、式で定義することもできます。
// Generator function expression const generatorFunction = function*() {}
ジェネレーターは、オブジェクトまたはクラスのメソッドにすることもできます。
// Generator as the method of an object const generatorObj = { *generatorMethod() {}, } // Generator as the method of a class class GeneratorClass { *generatorMethod() {} }
この記事全体の例では、ジェネレーター関数の宣言構文を使用します。
注:通常の関数とは異なり、ジェネレーターはnew
キーワードを使用して構築することも、矢印関数と組み合わせて使用することもできません。
ジェネレーター関数を宣言する方法がわかったので、それらが返す反復可能なGenerator
オブジェクトを見てみましょう。
ジェネレータオブジェクト
従来、JavaScriptの関数は最後まで実行され、関数を呼び出すと、return
キーワードに到達したときに値が返されます。 return
キーワードを省略すると、関数は暗黙的にundefined
を返します。
たとえば、次のコードでは、2つの整数引数の合計である値を返すsum()
関数を宣言します。
// A regular function that sums two values function sum(a, b) { return a + b }
関数を呼び出すと、引数の合計である値が返されます。
sum(5, 6) // 11
ただし、ジェネレータ関数はすぐに値を返すのではなく、反復可能なGenerator
オブジェクトを返します。 次の例では、関数を宣言し、標準関数のように単一の戻り値を与えます。
// Declare a generator function with a single return value function* generatorFunction() { return 'Hello, Generator!' }
ジェネレーター関数を呼び出すと、 Generator オブジェクトが返されます。これは、変数に割り当てることができます。
// Assign the Generator object to generator const generator = generatorFunction();
これが通常の関数である場合、generator
は関数で返される文字列を提供することを期待します。 ただし、実際に取得するのは、suspended
状態のオブジェクトです。 したがって、generator
を呼び出すと、次のような出力が得られます。
OutputgeneratorFunction {<suspended>} __proto__: Generator [[GeneratorLocation]]: VM272:1 [[GeneratorStatus]]: "suspended" [[GeneratorFunction]]: ƒ* generatorFunction() [[GeneratorReceiver]]: Window [[Scopes]]: Scopes[3]
関数によって返されるGenerator
オブジェクトは、iteratorです。 イテレータは、next()
メソッドを使用できるオブジェクトであり、一連の値を反復処理するために使用されます。 next()
メソッドは、value
およびdone
プロパティを持つオブジェクトを返します。 value
は戻り値を表し、done
はイテレーターがすべての値を実行したかどうかを示します。
これを知って、generator
でnext()
を呼び出し、イテレータの現在の値と状態を取得しましょう。
// Call the next method on the Generator object generator.next()
これにより、次の出力が得られます。
Output{value: "Hello, Generator!", done: true}
next()
の呼び出しから返される値はHello, Generator!
であり、done
の状態はtrue
です。これは、この値がreturn
からのものであるためです。イテレータを閉じました。 イテレータが実行されると、ジェネレータ関数のステータスがsuspended
からclosed
に変わります。 generator
を再度呼び出すと、次のようになります。
OutputgeneratorFunction {<closed>}
今のところ、ジェネレーター関数が関数のreturn
値を取得するためのより複雑な方法になる方法を示しただけです。 ただし、ジェネレーター関数には、通常の関数とは異なる独自の機能もあります。 次のセクションでは、yield
演算子について学習し、ジェネレーターが実行を一時停止および再開する方法を確認します。
yield
演算子
ジェネレーターはJavaScriptに新しいキーワードyieldを導入します。 yield
は、ジェネレーター関数を一時停止し、yield
に続く値を返すことができ、値を反復処理するための軽量な方法を提供します。
この例では、ジェネレーター関数を異なる値で3回一時停止し、最後に値を返します。 次に、Generator
オブジェクトをgenerator
変数に割り当てます。
// Create a generator function with multiple yields function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity' return 'The Oracle' } const generator = generatorFunction()
これで、ジェネレーター関数でnext()
を呼び出すと、yield
に遭遇するたびに一時停止します。 done
は各yield
の後にfalse
に設定され、ジェネレーターが終了していないことを示します。 return
が検出されるか、関数でyield
が検出されなくなると、done
はtrue
に切り替わり、ジェネレーターは終了します。 。
next()
メソッドを4回続けて使用します。
// Call next four times generator.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
を必要としないことに注意してください。 省略した場合、ジェネレータが完了した後のnext()
への後続の呼び出しと同様に、最後の反復は{value: undefined, done: true}
を返します。
ジェネレーターでの反復
next()
メソッドを使用して、Generator
オブジェクトを手動で繰り返し、完全なオブジェクトのすべてのvalue
およびdone
プロパティを受け取りました。 ただし、 Array 、 Map、およびSet と同様に、Generator
は反復プロトコルに従い、で反復できます。 for ... of :
// Iterate over Generator object for (const value of generator) { console.log(value) }
これにより、次が返されます。
OutputNeo Morpheus Trinity
スプレッド演算子を使用して、Generator
の値を配列に割り当てることもできます。
// Create an array from the values of a Generator object const values = [...generator] console.log(values)
これにより、次の配列が得られます。
Output(3) ["Neo", "Morpheus", "Trinity"]
Spreadとfor...of
はどちらも、return
を値に因数分解しません(この場合、'The Oracle'
になります)。
注:これらの方法はどちらも有限ジェネレーターの操作に効果的ですが、ジェネレーターが無限データストリームを処理している場合、スプレッドまたはfor...of
を直接使用せずに使用することはできません。無限ループを作成します。
ジェネレーターを閉じる
これまで見てきたように、ジェネレーターは、すべての値を反復処理することにより、done
プロパティをtrue
に設定し、ステータスをclosed
に設定できます。 ジェネレータをすぐにキャンセルするには、return()
メソッドとthrow()
メソッドの2つの追加の方法があります。
return()
を使用すると、return
ステートメントが関数本体にあるかのように、ジェネレーターをいつでも終了できます。 return()
に引数を渡すか、未定義の値の場合は空白のままにすることができます。
return()
を示すために、いくつかのyield
値を使用してジェネレーターを作成しますが、関数定義にはreturn
は含まれません。
function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity' } const generator = generatorFunction()
最初のnext()
は、done
がfalse
に設定された'Neo'
を提供します。 その直後に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
キーワードを無視しました。 これは、Promiseを直接キャンセルすることはできないため、ユーザーが別のアクションを実行したいときにWeb要求を中断するなど、関数をキャンセル可能にする必要がある非同期プログラミングで特に役立ちます。
ジェネレーター関数の本体にエラーをキャッチして処理する方法がある場合は、throw()
メソッドを使用して、ジェネレーターにエラーをスローできます。 これにより、ジェネレーターが起動し、エラーがスローされ、ジェネレーターが終了します。
これを示すために、ジェネレーター関数本体内に try ... catch を配置し、エラーが見つかった場合はログに記録します。
// Define a generator function with a try...catch function* generatorFunction() { try { yield 'Neo' yield 'Morpheus' } catch (error) { console.log(error) } } // Invoke the generator and throw an error const generator = generatorFunction()
次に、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
オブジェクトで使用できるメソッドのリストを示します。
方法 | 説明 |
---|---|
next()
|
ジェネレータで次の値を返します |
return()
|
ジェネレーターに値を返し、ジェネレーターを終了します |
throw()
|
エラーをスローしてジェネレーターを終了します |
次の表に、Generator
オブジェクトの可能な状態を示します。
状態 | 説明 |
---|---|
suspended
|
ジェネレータは実行を停止しましたが、終了していません |
closed
|
ジェネレータは、エラーが発生するか、戻るか、すべての値を反復処理することによって終了しました |
yield
委任
通常のyield
演算子に加えて、ジェネレーターは yield * 式を使用して、さらに値を別のジェネレーターに委任することもできます。 yield*
がジェネレーター内で検出されると、委任されたジェネレーター内に入り、そのジェネレーターが閉じられるまですべてのyield
の反復を開始します。 これを使用して、さまざまなジェネレーター関数を分離し、コードを意味的に整理しながら、すべてのyield
を正しい順序で反復可能にすることができます。
実例を示すために、2つのジェネレーター関数を作成できます。一方はyield*
が他方で動作します。
// Generator function that will be delegated to function* delegate() { yield 3 yield 4 } // Outer generator function function* begin() { yield 1 yield 2 yield* delegate() }
次に、begin()
ジェネレーター関数を繰り返します。
// Iterate through the outer generator const generator = begin() for (const value of generator) { console.log(value) }
これにより、生成された順序で次の値が得られます。
Output1 2 3 4
外側のジェネレーターは値1
と2
を生成し、yield*
で他のジェネレーターに委任され、3
と4
を返しました。
yield*
は、配列やマップなど、反復可能な任意のオブジェクトに委任することもできます。 yield
を使用したいジェネレーター内の関数もジェネレーターである必要があるため、歩留まりの委任はコードの整理に役立ちます。
無限のデータストリーム
ジェネレーターの便利な側面の1つは、無限のデータストリームとコレクションを処理する機能です。 これは、数値を1つインクリメントするジェネレーター関数内に無限ループを作成することで実証できます。
次のコードブロックでは、このジェネレーター関数を定義してから、ジェネレーターを開始します。
// Define a generator function that increments by one function* incrementer() { let i = 0 while (true) { yield i++ } } // Initiate the generator const counter = incrementer()
ここで、next()
を使用して値を繰り返し処理します。
// Iterate through the values counter.next() counter.next() counter.next() counter.next()
これにより、次の出力が得られます。
Output{value: 0, done: false} {value: 1, done: false} {value: 2, done: false} {value: 3, done: false}
この関数は、done
プロパティがfalse
のままである間、無限ループで連続する値を返し、終了しないようにします。
ジェネレーターを使用すると、実行を自由に停止および再開できるため、無限ループの作成について心配する必要はありません。 ただし、ジェネレータを呼び出す方法には注意が必要です。 無限データストリームでspreadまたはfor...of
を使用する場合でも、無限ループを一度に繰り返すことになり、環境がクラッシュします。
無限データストリームのより複雑な例として、フィボナッチジェネレーター関数を作成できます。 前の2つの値を連続的に加算するフィボナッチ数列は、ジェネレーター内の無限ループを使用して次のように記述できます。
// Create a fibonacci generator function function* fibonacci() { let prev = 0 let next = 1 yield prev yield next // Add previous and next values and yield them forever while (true) { const newVal = next + prev yield newVal prev = next next = newVal } }
これをテストするために、有限数をループして、フィボナッチ数列をコンソールに出力できます。
// Print the first 10 values of fibonacci const fib = fibonacci() for (let i = 0; i < 10; i++) { console.log(fib.next().value) }
これにより、次のようになります。
Output0 1 1 2 3 5 8 13 21 34
無限のデータセットを処理する機能は、ジェネレーターを非常に強力にするものの一部です。 これは、Webアプリケーションのフロントエンドに無限スクロールを実装するような例で役立ちます。
ジェネレーターで値を渡す
この記事全体を通して、イテレーターとしてジェネレーターを使用し、各反復で値を生成しました。 ジェネレータは、値を生成するだけでなく、next()
からの値を消費することもできます。 この場合、yield
には値が含まれます。
呼び出された最初のnext()
は値を渡さず、ジェネレーターを起動するだけであることに注意することが重要です。 これを示すために、yield
の値をログに記録し、いくつかの値を使用してnext()
を数回呼び出すことができます。
function* generatorFunction() { console.log(yield) console.log(yield) return 'The end' } const generator = generatorFunction() generator.next() generator.next(100) generator.next(200)
これにより、次の出力が得られます。
Output100 200 {value: "The end", done: true}
ジェネレータに初期値をシードすることもできます。 次の例では、for
ループを作成し、各値をnext()
メソッドに渡しますが、初期関数にも引数を渡します。
function* generatorFunction(value) { while (true) { value = yield value * 10 } } // Initiate a generator and seed it with an initial value const generator = generatorFunction(0) for (let i = 0; i < 5; i++) { console.log(generator.next(i).value) }
next()
から値を取得し、次の反復で新しい値を生成します。これは、前の値に10を掛けたものです。 これにより、次のようになります。
Output0 10 20 30 40
ジェネレーターの起動に対処する別の方法は、他のことを行う前に常にnext()
を1回呼び出す関数でジェネレーターをラップすることです。
async
/await
ジェネレーター付き
非同期関数は、ES6 + JavaScriptで使用できる関数の一種であり、非同期データを同期的に表示することで、非同期データの操作を理解しやすくします。 ジェネレーターには、非同期関数よりも広範な機能がありますが、同様の動作を複製することができます。 この方法で非同期プログラミングを実装すると、コードの柔軟性を高めることができます。
このセクションでは、ジェネレーターを使用して async /awaitを再現する例を示します。
FetchAPIを使用してJSONPlaceholderAPI (テスト目的で JSON データの例を提供)からデータを取得し、応答をログに記録する非同期関数を作成してみましょう。コンソール。
APIからデータをフェッチしてオブジェクトの配列を返すgetUsers
という非同期関数を定義することから始め、次にgetUsers
を呼び出します。
const getUsers = async function() { const response = await fetch('https://jsonplaceholder.typicode.com/users') const json = await response.json() return json } // Call the getUsers function and log the response getUsers().then(response => console.log(response))
これにより、次のようなJSONデータが得られます。
Output[ {id: 1, name: "Leanne Graham" ...}, {id: 2, name: "Ervin Howell" ...}, {id: 3, name": "Clementine Bauch" ...}, {id: 4, name: "Patricia Lebsack"...}, {id: 5, name: "Chelsey Dietrich"...}, ...]
ジェネレーターを使用すると、async
/await
キーワードを使用しないほぼ同じものを作成できます。 代わりに、await
の約束の代わりに、作成した新しい関数とyield
の値を使用します。
次のコードブロックでは、getUsers
という関数を定義します。この関数は、新しいasyncAlt
関数(後で説明します)を使用して、async
/[X158Xを模倣します。 ]。
const getUsers = asyncAlt(function*() { const response = yield fetch('https://jsonplaceholder.typicode.com/users') const json = yield response.json() return json }) // Invoking the function getUsers().then(response => console.log(response))
ご覧のとおり、値を生成するジェネレーター関数が渡されることを除いて、async
/await
の実装とほぼ同じに見えます。
これで、非同期関数に似たasyncAlt
関数を作成できます。 asyncAlt
には、パラメーターとしてジェネレーター関数があります。これは、fetch
が返すことを約束する関数です。 asyncAlt
は関数自体を返し、最後の約束まで見つかったすべての約束を解決します。
// Define a function named asyncAlt that takes a generator function as an argument function asyncAlt(generatorFunction) { // Return a function return function() { // Create and assign the generator object const generator = generatorFunction() // Define a function that accepts the next iteration of the generator function resolve(next) { // If the generator is closed and there are no more values to yield, // resolve the last value if (next.done) { return Promise.resolve(next.value) } // If there are still values to yield, they are promises and // must be resolved. return Promise.resolve(next.value).then(response => { return resolve(generator.next(response)) }) } // Begin resolving promises return resolve(generator.next()) } }
これにより、async
/await
バージョンと同じ出力が得られます。
Output[ {id: 1, name: "Leanne Graham" ...}, {id: 2, name: "Ervin Howell" ...}, {id: 3, name": "Clementine Bauch" ...}, {id: 4, name: "Patricia Lebsack"...}, {id: 5, name: "Chelsey Dietrich"...}, ...]
この実装は、async
/ await
の代わりにジェネレーターを使用する方法を示すためのものであり、本番環境に対応した設計ではないことに注意してください。 エラー処理は設定されておらず、生成された値にパラメーターを渡す機能もありません。 この方法はコードに柔軟性を加えることができますが、実装の詳細を抽象化し、生産的なコードの記述に集中できるため、多くの場合async/await
の方が適しています。
結論
ジェネレーターは、実行を停止および再開できるプロセスです。 これらは、一般的には使用されていませんが、JavaScriptの強力で用途の広い機能です。 このチュートリアルでは、ジェネレーター関数とジェネレーターオブジェクト、ジェネレーターで使用できるメソッド、yield
およびyield*
演算子、および有限および無限のデータセットで使用されるジェネレーターについて学習しました。 また、ネストされたコールバックや長いPromiseチェーンを使用せずに非同期コードを実装する1つの方法についても検討しました。
JavaScript構文の詳細については、 JavaScript でのこれ、バインド、呼び出し、および適用の理解とJavaScriptでのオブジェクトのマップと設定の理解のチュートリアルを参照してください。