JavaScriptのジェネレータ関数とイテレータの仕組みをGIFアニメで解説

JavaScriptのジェネレータ関数とイテレータの仕組みをGIFアニメで解説した記事を紹介します。

JavaScriptのジェネレータ関数とイテレータの仕組みをGIFアニメで解説

💡🎁 JavaScript Visualized: Generators and Iterators
by Lydia Hallie

下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。

ES6ではジェネレータ関数と呼ばれるクールなものが導入されました🎉
ジェネレータ関数について尋ねると多くの人は、「知っているけど、混乱して二度と見ていない」「ブログで読んでみたけど、よく分からない」「分かるけど、みんななんで使ってるの」🤔
私も昔は同じように思っていました。しかし!実はかなりクールです!!

ジェネレータ関数とは何でしょうか?
まずは、通常の昔ながらの関数を見てましょう👵🏼

昔ながらの関数

昔ながらの関数

これについては何も特別なことはありません。ごく普通の関数で、値を4回ログに記録するだけの関数です。
では、実行してみましょう!

関数の実行

「通常の関数を見させて、なぜリディア(著者)は私の人生で貴重な5秒を無駄にしたの?」非常に良い質問です。通常の関数は、run-to-completionモデルと呼ばれるものに従っています。関数を呼び出すと、関数は完了するまで常に実行されます(エラーがない限り)。関数の途中でランダムに一時停止することはできません。

そして、ここがクールなところで、ジェネレータ関数はrun-to-completionモデルに従いません!🤯 これはジェネレータ関数を実行中にランダムに一時停止できることを意味するのでしょうか? まぁ、そんなところです。
ジェネレータ関数とは何か、どのように使えるのか見てましょう!

functionの後にアスタリスク*を記述して、ジェネレータ関数を作成します。

ジェネレータ関数を作成

ジェネレータ関数を作成

しかし、ジェネレータ関数を使用するために必要なことはこれだけではありません。ジェネレータ関数は通常の関数とは全く異なる方法で動作します。

  • ジェネレータ関数を呼び出すと、イテレータであるジェネレータオブジェクトが返されます。
  • ジェネレータ関数でyieldキーワードを使用して、実行を一時停止することができます。

それって何の意味があるの!?

まず、最初の関数を見てましょう。
ジェネレータ関数を呼び出すと、ジェネレータオブジェクトが返されます。通常の関数を呼び出すと、関数本体が実行され、最終的には値が返されます。しかし、ジェネレータ関数を呼び出すと、ジェネレータオブジェクトが返されます!
返された値をログに記録するとどうなるか見てみましょう。

ジェネレータ関数を実行

これには少し圧倒されたかもしれません、心の中で(あるいは実際に🙃)叫んでいるあなたの声が聞こえてきます。
しかし、心配しないでください。ここにあるようなプロパティを実際に使用する必要はありません。では、ジェネレータオブジェクトは何に使用するのでしょうか?

少し戻り、通常の関数とジェネレータ関数の2つ目の違いに答えます。ジェネレータ関数では、yieldキーワードを使用して、実行を一時停止できます。

ジェネレータ関数では、次のように記述します(genFuncgeneratorFunctionの略です)。

yieldキーワードの記述例

yieldキーワードの記述例

yieldキーワードは何をしているのでしょうか? ジェネレータの実行はyieldキーワードが検出されると、一時停止されます。そして、一番良いのは、次に関数を実行した時に以前に一時停止した場所を覚えていて、そこから実行してくれることです😃 ここで何が起こっているのかは後ほど説明するので、心配しないでください。

  1. 最初の実行時では、1行目で一時停止し、「✨」を生成します。
  2. 2回目の実行時では、前のyieldキーワードの行から始まります。その後、2番目のyieldキーワードまで実行され、「💕」を生成します。
  3. 3回目の実行では、前のyieldキーワードの行から始まります。その後、returnキーワードに遭遇するまで実行され、「Done!」を返します。

でも前に、ジェネレータ関数を呼び出すとジェネレータオブジェクトが返されるのを見ていたのに、どのように関数を呼び出すことができるのでしょうか?🤔
そこでジェネレータオブジェクトの出番です!

ジェネレータオブジェクトにはnextメソッド(プロトタイプチェーン上)が含まれています。このメソッドはジェネレータオブジェクトを反復処理するために使用するものです。ただし、値を生成した後に中断した状態を記憶するには、ジェネレータオブジェクトを変数に割り当てる必要があります。私はこれをgeneratorObjectの略で、genObjと呼ぶことにします。

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

そうです、前に見たのと同じような恐ろしいオブジェクトです。genObjジェネレータオブジェクトでnextメソッドを呼び出すとどうなるか見てみましょう。

nextメソッドを呼び出す

ジェネレータは最初のyieldキーワードに遭遇するまで実行されました! これはvalueプロパティとdoneプロパティを含むオブジェクトを生成しました。

{ value: ... , done: ... }

valueプロパティは、取得した値と等しくなります。doneプロパティはブール値で、ジェネレータ関数が値を返した場合のみtrueになります(yieldedではありません!😊)。

ジェネレータの反復処理を停止したので、関数が一時停止したように見えます。なんてクールなのでしょう。nextメソッドをもう一度呼び出しましょう!😃

nextメソッドをもう一度呼び出す

まず、コンソールにFirst log!という文字列を記録され、これはyieldキーワードでもreturnキーワードでもないので続きます! 次に、値💕yieldキーワードに遭遇しました。オブジェクトは、💕valueプロパティとdoneプロパティで生成されます。まだジェネレータから返されていないため、doneプロパティの値もfalseです。

最後にnextを発動させましょう。

nextを発動

文字列Second log!がコンソールに記録されました。次に、Doneという値のreturnキーワードに遭遇しました。オブジェクトはDone!valueプロパティが返されます。今回は実際に返ってきたので、doneの値はtrueになります。

doneプロパティは実はとても重要です。ジェネレータオブジェクトの反復処理は一度しかできません。nextメソッドをもう一度呼び出すとどうなるでしょうか?

nextメソッドを呼び出す

これは永遠にundefinedをただ返すだけです。もう一度反復処理を行いたい場合は、新しいジェネレータオブジェクトを作成するだけです。

今見たように、ジェネレータ関数はイテレータ(ジェネレータオブジェクト)を返します。しかし、、、イテレータとは何でしょうか。これはfor ofループ、そして返されたオブジェクトのスプレッド演算子を使用できることでしょうか? ばんざーい!!!🤩

[....]構文を使用して、生成された値を配列に分散させてみましょう。

[....]構文を使用して

もしくは、for ofループを使用してみます。

for ofループを使用して

非常に多くの可能性があります!

しかし、何がイテレータをイテレータにしているのでしょうか? なぜならfor ofループ、配列、文字列、マップ、セットを使ったスプレッド構文も使用できるからです。これは実際には、[Symbol.iterator]というイテレータプロトコルを実装しているためです。
例えば、次の値があるとしましょう(非常に分かりやすい名前ですが💁🏼)。

イテレータプロトコル

array, string, generatorObjectはすべてイテレータです。これらの[Symbol.iterator]プロパティの値を見てましょう。

[Symbol.iterator]プロパティの値

しかし、イテレータ反復可能ではない値に対する[Symbol.iterator]の値はどうなるのでしょうか?

反復可能ではない値の[Symbol.iterator]の値

そうです、それだけではありません。では、、、単に手動で[Symbol.iterator]プロパティを追加して、非反復可能オブジェクトを反復可能にすればよいのでしょうか? はい、できます!😃

[Symbol.iterator]は前に見たようにオブジェクトを返すnextメソッドを含むイテレータを返す必要があります。{ value: '...', done: false/true }

シンプルに保つために、デフォルトでイテレータを返すために、[Symbol.iterator]の値をジェンレーター関数に等しく設定できます。オブジェクトを反復可能にし、生成された値をオブジェクト全体にします。

生成された値をオブジェクト全体に

objectオブジェクトに対してスプレッド構文またはfor-ofループを使用するとどうなるかを確認してください。

スプレッド構文またはfor-ofループを使用するとどうなるか

あるいは、オブジェクトのキーだけを取得したい場合もあります。「ああ、これなら簡単だ、thisの代わりに、Object.keys(this)を生成するだけだ!」

オブジェクトのキーだけを取得したい場合

試してみましょう。

入れ子になった配列を作成

あ、しまった! Object.keys(this)は配列なので、生成された値は配列です。そして、この生成された配列を別の配列に分散し、入れ子になった配列を作成してしまいました。こうではなく、個々のキーを生成したかっただけです!

朗報があります!🥳 yield*キーワードを使用すれば、ジェネレータ内のイテレータから個々の値を生成できます、アスタリスク付きのyieldです! 例えば、最初にアボカドを生成するジェネネーター関数があり、次に別のイテレータ(この場合は配列)の値を個別に生成したいとします。この場合は、yield*キーワードを使用します。そして、別のジェネレータにデリゲートします!

yield*を使用する

デリゲートされたジェネレータの各値は、genObjイテレータの反復処理を続ける前に、生成されます。

これこそが、オブジェクトのキーを個別に取得するために必要なことなのです!

キーを個別に取得

ジェネレータ関数のもう一つの用途は、オブザーバー関数として使用できることです。ジェネレータは入ってくるデータを持つことができ、そのデータが渡された場合のみそれを処理します。
例えば、

オブザーバー関数として使用

ここでの大きな違いは、前の例で見たような単にyield [value]を持つのではないということです。その代わりに、secontという値を代入して、yield値にFirst!という文字列を代入します。これはnextメソッドを呼び出すときに生成される値です。

イテレータブルで初めてnextメソッドを呼び出すとどうなるか見てましょう。

nextメソッドを呼び出す

1行目でyieldに遭遇して、値First!が生成されました。
では、変数secondの値は何でしょう?

実はこれがnextメソッドを呼び出すときに渡す値です! 今回はI like JavaScriptとうい文字列を渡します。

ここで重要なのは、nextメソッドの最初の呼び出しでは、まだ何の入力も記録していないということです。オブザーバーを最初に起動するだけです。ジェネレータは続行する前に入力を待ち、nextメソッドに渡す値を処理します。

では、なぜジェネレータ関数を使おうと思ったのでしょうか?

ジェネレータの最大の利点の1つは、遅延評価されることです。つまり、nextメソッドを呼び出し後に返される値は、具体的に要求した後でのみ計算されることを意味します! 通常の関数にはこれがありません。将来的にそれを使用する必要がある場合に備えて、すべての値が生成されます。

他にも使用例がありますが、大規模なデータセットの反復処理をおこなう際には、通常これを使用してコントロールを強化します。

例えば、ブッククラブのリストがあるとしましょう📚 簡潔にするために大勢のメンバーではなく、1人のメンバーしかいないとします。メンバーは現在book配列で表されている何冊かの本を読んでいます。

本のリスト

ey812というIDの本を探しています。これを見つけるためには、入れ子になったfor-loopやforEachヘルパーを使用することができますが、これでは探しているメンバーを見つけた後でも、データを繰り返し処理することになってしまいます。

ジェネレータの素晴らしいところは、指示がない限り、実行し続けることがないということです。つまり、返された各アイテムを評価することができ、それが探しているアイテムであれば、nextを呼ばないということです!
どのようになるか見てみましょう。

まず、チームメンバーのbook配列を反復処理するジェネレータを作成します。チームメンバーのbook配列を関数に渡し、配列を反復処理して、それぞれの本を生成します。

それぞれの本を生成

パーフェクト! 次に、clubMembers配列を反復処理するジェネレータを作成する必要があります。クラブメンバー自体には関心がありませんので、本を反復処理する必要があります。iterateMembersジェネレータで、本を生成するためだけにiterateBooksイテレータをデリゲートしてみましょう!

本を生成

あと少しです! 最後のステップは、ブッククラブの反復処理です。前の例と同じように、ブッククラブ自体には関心がないので、本のみにします。iterateClubMembersイテレータをデリゲートして、clubMembers配列を渡します。

clubMembers配列を渡す

これをすべて反復処理するためには、bookClub配列をiterateBookClubsジェネレータに渡して、ジェネレータオブジェクトを反復可能にする必要があります。とりあえず、ジェネレータオブジェクトをイテレータ用にitと呼ぶことにします。

itを定義

nextメソッドを呼び出して、idey812の本を取得します。

ey812の本を取得

うまくいきました!
探している本を見つけるために、すべてのデータを反復処理する必要はありません。その代わりに、オンデマンドでデータを探せばいいのです。もちろん、毎回手動でnextメソッドを呼び出すのはあまり効率的ではありません。そこで、代わりに関数を作ってみましょう!

関数にidを渡します、このidは探している本のidです。value.idが探しているidの場合は、value全体(bookオブジェクト)を返すだけです。正しいidではない場合は、もう一度nextを呼び出します!

正しいidではない場合は、nextを呼び出す
aaaaaa

もちろん、これは小さなデータセットです。膨大な量のデータから1つの値を見つけるために解析する必要があると想像してみてください。通常、解析を開始するには、データセット全体の準備が整うのを待つ必要があります。しかし、ジェネレータ関数を使用すれば、データの小さな塊を要求し、そのデータをチェックするだけで、nextメソッドを呼び出したときのみ値が生成されます。

「読んだだけでは理解できない」と心配する必要はありません。ジェネレータ関数は実際に使用してみるまで、かなり混乱してしまうものです。しかし、いくつかの用語が少しでも明確になったかなと思います。
不明点があれば、@lydiahallieまで気軽に連絡してください!😊

sponsors

top of page

©2020 coliss