JavaScriptのジェネレーターを理解する

提供:Dev Guides
移動先:案内検索

著者は、 Open Internet / Free Speech Fund を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

ECMAScript 2015 では、ジェネレーターがJavaScript言語に導入されました。 ジェネレーターは、一時停止および再開できるプロセスであり、複数の値を生成できます。 JavaScriptのジェネレーターは、ジェネレーター関数で構成され、反復可能なGeneratorオブジェクトを返します。

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

この記事では、ジェネレーター関数を作成する方法、Generatorオブジェクトを反復処理する方法、ジェネレーター内のyieldreturnの違い、およびその他の側面について説明します。ジェネレーターの操作。

ジェネレーター機能

ジェネレーター関数は、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はイテレーターがすべての値を実行したかどうかを示します。

これを知って、generatornext()を呼び出し、イテレータの現在の値と状態を取得しましょう。

// 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が検出されなくなると、donetrueに切り替わり、ジェネレーターは終了します。 。

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プロパティを受け取りました。 ただし、 ArrayMap、および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()は、donefalseに設定された'Neo'を提供します。 その直後にGeneratorオブジェクトでreturn()メソッドを呼び出すと、渡された値が取得され、donetrueに設定されます。 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

外側のジェネレーターは値12を生成し、yield*で他のジェネレーターに委任され、34を返しました。

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でのオブジェクトのマップと設定の理解のチュートリアルを参照してください。