イテレーターとジェネレーター

コレクション/配列要素の処理は、一般的で頻繁な操作です。 JavaScriptには、単純なfor(;;)



およびfor a in b



で始まるコレクションをトラバースする方法がいくつかあります



 var divs = document.querySelectorAll('div'); for (var i = 0, c = divs.length; i < c; i++) { console.log(divs[i].innerHTML); }
      
      





 var obj = {a: 1, b: 2, c: 3}; for (var i in obj) { console.log(i, obj[i]); }
      
      





Arrayオブジェクトには、 map(), filter()



すべての要素に対する走査メソッドがあります

 var numbers = [1, 2, 3, 4, 5]; var doubled = numbers.map(function (item) { return item * 2; }); console.log(doubled);
      
      





Firefoxには配列内包表記があります

 var numbers = [1, 2, 3, 4]; var doubled = [i * 2 for each (i in numbers)]; console.log(doubled); // [2, 4, 6, 8]
      
      





イテレーターとジェネレーターは、JavaScript 1.7(Mozillaによる)でFirefox 2+に登場しました(ほとんどすべてのブラウザーで松葉杖で「エミュレート」する方法については記事で説明します)イテレーターとジェネレーターfor in



、オブジェクトのリスト内の次の要素を取得するプロセスをカプセル化します。



多くの場合、配列の要素を記述して処理するために、大規模な構造を記述します。多くの場合、その一部をコピーして貼り付けます。 ジェネレーターとイテレーターのタスクは、構文糖を追加してこのプロセスを改良することです。



イテレータ



JavaScriptでは、ほとんどすべてのオブジェクトをバイパスできることを誰もが知っています。

配列

 var object = [1,2,3]; for (var i in object) { console.log(i, object[i]); }
      
      





対象

 var object = {a:1, b:2, c:3}; for (var i in object) { console.log(i, object[i]); }
      
      





ひも

 var object = 'abc'; for (var i in object) { console.log(i, object[i]); }
      
      





ある種のリスト

 var object = document.querySelectorAll('div'); for (var i in object) { if (object.hasOwnProperty(i)) { console.log(i, object[i]); } }
      
      





すべてのオブジェクトを順番に移動し、シーケンスを制御することはできません。 イテレータにも同様のインターフェイスがありますが、要素を制御する機能を提供します。



イテレータは、このシーケンスで現在の位置(カーソル)を維持しながら、コレクションの要素に1つずつアクセスする方法を知っているオブジェクトです。 JavaScriptでは、イテレータは、シーケンス内の次のオブジェクトを返すnext()



メソッドを持つオブジェクトです。 このメソッドは、シーケンスが終了したときにStopIteration



例外をスローする場合があります。



イテレータはさまざまな方法で使用できます。next next()



メソッドを直接呼び出すか、 for...in



またはfor each



(FFのみ)構文を使用します。



イテレータを作成する方法はいくつかあります。



イテレータ関数を呼び出す


 var lang = { name: 'JavaScript', birthYear: 1995 }; var it = Iterator(lang); // <<<
      
      





イテレータを作成した後、 next()



を使用してit



オブジェクトを反復できます



 var pair = it.next(); //  ["name", "JavaScript"] pair = it.next(); //  ["birthYear", 1995] pair = it.next(); //    StopIteration
      
      





回避策として、 for...in



またはfor each



使用できます。 バイパスは、 StopIteration



例外がStopIteration



されるとすぐに停止します。

 var it = Iterator(lang); for (var pair in it) { console.log(pair); // 2  [key, value] }
      
      





Iterator()



利点の1つは、 object.hasOwnProperty



をチェックする必要がないクロール時に、独自のプロパティ( querySelectorAll



クロールの例を思い出してください)のみをバイパスすることです。



Iterator()



は配列にも使用できます:

 var langs = ['JavaScript', 'Python', 'C++']; var it = Iterator(langs); for (var pair in it) { console.log(pair); // [index, language] }
      
      





2番目の引数がIterator



関数に渡される場合、インデックスのみが反復されます。
 var langs = ['JavaScript', 'Python', 'C++']; var it = Iterator(langs, true); for (var pair in it) { console.log(pair); // 0, 1, 2 }
      
      





インデックスと値については、 let



と破壊的な代入を使用して独自の変数を強調表示できます(FFのみ)。

 var langs = ['JavaScript', 'Python', 'C++']; var it = Iterator(langs); for (let [i, lang] in it) { //  FF console.log(i + ': ' + lang); //  "0: JavaScript" . }
      
      





このイテレータの作成方法はあまり有用ではなく、 Iterator



なしの通常for in



と似ています。



イテレーターを作成する


一部のオブジェクトは、特別な方法で反復する必要がある要素のコレクションです。 例:

-Rangeオブジェクトの反復は、セット内の数値を返す必要があります。

-広葉樹の最初の検索または深さ検索を使用して木の葉をバイパスできます。

-データベースへのクエリの結果は、1つのレコードでデータセットを反復して返すことができるオブジェクトとして返されます。

-無限の数学的シーケンスの反復子(たとえば、フィボナッチ数列)は、無限の長さの配列を作成せずに、結果を1つずつ返す必要があります。



範囲を保存する簡単なRangeオブジェクトを作成しましょう。

 function Range(low, high){ this.low = low; this.high = high; }
      
      





次に、この範囲からシーケンスを返すイテレータを作成します。 イテレータインターフェイスでは、シーケンス要素を返し、最後にStopIteration



例外をスローするnext()



メソッドを作成する必要があります。



 function RangeIterator(range){ this.range = range; this.current = this.range.low; } RangeIterator.prototype.next = function(){ if (this.current > this.range.high) throw StopIteration; else return this.current++; };
      
      







 var ri = new RangeIterator(new Range(1, 10)); ri.next(); // ... ri.next(); // StopIteration
      
      







どういうわけかあまり便利ではありません。 RangeIterator



コンストラクターRangeIterator



__iterator__



する__iterator__



は、 Range



オブジェクトで__iterator__



メソッドを使用します。 このメソッドは、 Range



オブジェクトの要素を反復処理するときに呼び出され、反復子ロジックを含むRangeIterator



を返す必要があります。



 Range.prototype.__iterator__ = function(){ return new RangeIterator(this); }; var range = new Range(3, 5); for (var i in range) { console.log(i); // 3, 4, 5 }
      
      





十分に快適ですが、多くの不必要。



発電機



任意のイテレータは便利ですが、作成するには注意深いプログラミングと内部状態の記憶が必要です。 ジェネレータは、より一般的な代替手段です。 それらは、その状態を記憶する単一の関数を使用して代替アルゴリズムを定義することを可能にします(有限状態マシンに似ています)。



ジェネレーターは、イテレーターを作成するファクトリーのように機能する特別なタイプの関数です。 少なくとも1つのyield



を含む関数はジェネレーターになります。



注:オリジナルでは、ジェネレーターはFirefox 2+でJavaScript <script type="application/javascript;version=1.7">



オーバーライドされたバージョンでのみ利用可能です



ジェネレーター関数が呼び出されると、その本体は実行されません。 Iterator-Generatorを作成して返すコンストラクターとして機能します。 Iterator-Generatorメソッドnext()



を呼び出すたびに、 yield



が返されるまで関数の本体が実行され、結果が返されます。 関数がreturn



終了するか、関数の本体が終了すると、 StopIteration



例外がスローされ、イテレーターは動作を停止します。



例で説明しましょう:

 function simpleGenerator(){ yield "first"; yield "second"; yield "third"; for (var i = 0; i < 3; i++) { yield i; } } var g = simpleGenerator(); console.log(g.next()); // "first" console.log(g.next()); // "second" console.log(g.next()); // "third" console.log(g.next()); // 0 console.log(g.next()); // 1 console.log(g.next()); // 2 console.log(g.next()); // StopIteration
      
      





Generator関数は、 __iterator__



オブジェクトの__iterator__



メソッドとして使用できます。 これにより、コードの量が大幅に削減されます。 ジェネレータを使用してRangeで例を書き換えます:

 function Range(low, high){ this.low = low; this.high = high; } Range.prototype.__iterator__ = function(){ for (var i = this.low; i <= this.high; i++) { yield i; } }; var range = new Range(3, 5); for (var i in range) { console.log(i); // 3, 4, 5 }
      
      





3倍短くなった



ジェネレーターを使用すると、無限のシーケンスを作成できます。 フィボナッチ数の例を見てみましょう:

 function fibonacci(){ var fn1 = 1; var fn2 = 1; while (1){ var current = fn2; fn2 = fn1; fn1 = fn1 + current; yield current; } } var sequence = fibonacci(); console.log(sequence.next()); // 1 console.log(sequence.next()); // 1 console.log(sequence.next()); // 2 console.log(sequence.next()); // 3 console.log(sequence.next()); // 5 console.log(sequence.next()); // 8 // ...
      
      





ジェネレーター関数には引数を渡すことができます。 StopIteration



またはreturn



を使用してジェネレーターを中断できます。 limit



引数を使用してフィボナッチ数で例を書き換えます。

 function fibonacci(limit){ var fn1 = 1; var fn2 = 1; while (1) { var current = fn2; fn2 = fn1; fn1 = fn1 + current; if (limit && current > limit) { return; } yield current; } } var sequence = fibonacci(7); console.log(sequence.next()); // 1 console.log(sequence.next()); // 1 console.log(sequence.next()); // 2 console.log(sequence.next()); // 3 console.log(sequence.next()); // 5 console.log(sequence.next()); // StopIteration
      
      





高度なジェネレーター



ジェネレータは、リクエストに応じて次の値を計算します。 これにより、上記の例のように、計算が困難なシーケンスまたは無限のシーケンスを作成できます。



next()



メソッドに加えて、Generator-Iteratorにはsend()



メソッドがあり、ジェネレーターの内部状態を変更できます。 send()



渡される値は、ジェネレーターを停止した最後のyield



結果として解釈されます。 send()



呼び出す前に、 next()



ジェネレーターを起動する必要があります。



次に、 send()



を使用してシーケンスを再起動する書き換えられたフィボナッチジェネレーターを示します。

 function fibonacci(){ var fn1 = 1; var fn2 = 1; while (1){ var current = fn2; fn2 = fn1; fn1 = fn1 + current; var reset = yield current; if (reset){ fn1 = 1; fn2 = 1; } } } var sequence = fibonacci(); print(sequence.next()); // 1 print(sequence.next()); // 1 print(sequence.next()); // 2 print(sequence.next()); // 3 print(sequence.next()); // 5 print(sequence.send(true)); // 1 print(sequence.next()); // 1 print(sequence.next()); // 2 print(sequence.next()); // 3
      
      





throw()



メソッドを呼び出すことにより、ジェネレーターに強制的に例外をスローさせることができます。 このメソッドは1つの引数を取り、ジェネレーターをthrow value;



throw value;



)。 この例外は、ジェネレータの現在停止しているコンテキスト(最後のyield



た場所)からスローされます。



ジェネレーターが開始されていない場合( next()



メソッドの呼び出しがなかった場合)、 throw()



next()



を呼び出し、 yield



場所で例外をスローします。



close()



メソッドを使用してジェネレーターを閉じることができます:

このメソッドは次のことを行います。

-すべてのfinally



ブロックが実行されます

StopIteration



ブロックがStopIteration



以外の例外をスローする場合、その例外はclose()



メソッドが呼び出されたコンテキストにスローされます

-ジェネレーターは破壊されます



式ジェネレーター



「配列プレースホルダー」は、呼び出されると1つの重大な欠点があります。これは、配列を埋めてメモリを占有します。 配列が小さい場合は目立たず、大きな配列の場合は非常に高価です。また、要素の数が無限の場合は、「配列プレースホルダー」は意味がありません。



ジェネレーターは、オンデマンドの計算を実行します。 式ジェネレーターは配列プレースホルダーに似ていますが、式ジェネレーターは配列を作成する代わりに、オンデマンドで実行される反復子ジェネレーターを作成します。 それらを短いジェネレーターと呼ぶことができます。



多数の整数を走査する反復子があるとします。 重複する番号を反復処理する新しいイテレータを作成します。 「配列プレースホルダー」を使用して、乗算値を含む配列全体をメモリに作成します。

 var doubles = [i * 2 for (i in it)];
      
      





式ジェネレーターは、必要に応じて数値を乗算する新しいイテレーターを作成します。

 var it2 = (i * 2 for (i in it)); print(it2.next()); //    print(it2.next()); //   
      
      





式ジェネレーターは、引数として関数に渡すことができます。 この場合の追加の括弧は省略できます。

 var result = doSomething(i * 2 for (i in it));
      
      









数字の2倍のFinobacciシーケンスは非常にクールですが、どういうわけか生命からは程遠いものです。 人生の例をいくつか見てみましょう。



1. DOMツリーをバイパスします


このページにはいくつかのリンクがあります。 それぞれについて、ドメインとinnerHTML



を取得する必要があり、 innerHTML



がドメインと一致する場合、コンソールにドメインを表示します。



テストリンク:

 <a href="http://ya.ru/"> </a> <a href="http://google.ru/"></a> <a href="http://habrahabr.ru/"></a> <a href="http://twitter.com/">twitter.com</a>
      
      





ジェネレーターへのコード:

 var aList = document.querySelectorAll('a'); for (var i = 0, c = aList.length, a, content, domain; i < c; i++) { a = aList[i]; content = a.innerHTML; domain = a.getAttribute('href').replace('http://', '').split('/').shift(); if (content === domain) { console.log(domain); } }
      
      





すべてが自然で明確です。 そして、このサイクルを他のチェックに使用する必要がある場合は? コードをコピーしますか? ジェネレーターが役立ちます;)書き換え:

  //    type="application/javascript;version=1.7" var aDomainContentGenerator = function () { var aList = document.querySelectorAll('a'); for (var i = 0, c = aList.length, a, content, domain; i < c; i++) { a = aList[i]; content = a.innerHTML; domain = a.getAttribute('href').replace('http://', '').split('/').shift(); yield {domain: domain, content: content}; } }; var ancors = aDomainContentGenerator(); //  -      //   for c if,   . ,     var ancorsWithSameContentAndDomain = (item for (item in ancors) if (item.content === item.domain)); for (item in ancorsWithSameContentAndDomain) { console.log(item.domain); }
      
      





コードはaDomainContentGenerator()



が、コードの幅が大きくなり(これは重要なプラスです)、 aDomainContentGenerator()



ジェネレーターを他の目的に使用できるようになりaDomainContentGenerator()







2.ツリーウォーク


行が散在するツリーがあります。 昇順で表示する必要があります。

 function Tree(left, label, right) { this.left = left; this.label = label; this.right = right; } //      function inorder(t) { if (t) { for (var item in inorder(t.left)) { yield item; } yield t.label; for (var item in inorder(t.right)) { yield item; } } } //    function make(array) { //  : if (array.length == 1) return new Tree(null, array[0], null); return new Tree(make(array[0]), array[1], make(array[2])); } var tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]); //  for (var node in inorder(tree)) { console.log(node); // a, b, c, d, ... }
      
      





コードに1つの大きな構造がありました

 for (var item in inorder(t.right)) { yield item; }
      
      





将来、これらは次のものに置き換えられます:

 yield for inorder(t.right);
      
      





ジェネレーターは


-反復可能なオブジェクトの美しいトラバース:リスト(ファイル内の行、データベースへのクエリの結果)、ツリーの再帰トラバース(XML、HTML、バイナリ、およびその他の任意)、範囲オブジェクトの作成とトラバース(0..10)

-アクセスログをバイパスします。 たとえば、転送されたデータの量に関する情報を収集する必要があるローテーションされたアクセスログがいくつかあります。

-yieldはデバッグ時にブレークポイントとして機能します

-長いサイクルをいくつかの部分に分割するために使用できます(時間と処理されたサイクル要素の数の両方で)

-擬似乱数またはシーケンスジェネレーター

-ウィザード/マスターの基礎(ステップバイステップのインターフェイス)

-イベントの動作を変更するために使用できます(たとえば、ボタンをクリックする)



発電機の利点



1.オンデマンド計算を実行する

2.余分なメモリを使用しないでください

3.コレクションアイテムを取得するプロセスをカプセル化する

4.コードを伸ばす

5.ジェネレーターは、パイプラインに接着することでフィルターとして使用できます



性能



Pythonテストから判断すると、ジェネレーターは通常のサイクルに劣りません。 詳細については、プレゼンテーション「 システムプログラマ向けジェネレータトリックの概要 」(スライド22)を参照してください。

Generator vs Common Loopテストによると、 ジェネレーターはサイクルよりも3分の1遅くなります。



いつ使用できますか?



オーバーライドを使用したFirefox 2+では、JavaScriptバージョンをすぐに作成できます。 残りは悪化しています。 1つの優れたプロジェクトがあります-Google traceur-compiler。 このコンパイラにより、ES6 +の将来のバージョンのコードをES3コードに処理できます。 しかし、これは万能薬ではありません。例「ツリーウォーキング」のコードはどのブラウザでも機能しませんでした

TCはジェネレーターのfor..inループを理解しません:

 for (var node in inorder(tree)) { console.log(node); // a, b, c, d, ... }
      
      





彼が必要

 for (let node : inorder(tree)) { console.log(node); // a, b, c, d, ... }
      
      





しかし、このビューはFirefoxを理解しません。 TCはFunction#bind



Object.defineProperty



必要とFunction#bind



ため、コンパイルされたバージョンはTC作成者のブラウザObject.defineProperty



Chromeでのみ機能します:)。 コンパイルされたジェネレーターのコードは、有限状態マシンを使用し、 try catch



やその他の変質者を中心に踊ります。 そのような「コード」が判明します。

 function Tree(left, label, right) { this.left = left; this.label = label; this.right = right; } function inorder(t) { var $that = this; return { __traceurIterator__: function() { var $state = 20; var $storedException; var $finallyFallThrough; var $__1; var $__2; var $__3; var $__4; var $result = { moveNext:(function() { while(true) try { switch($state) { case 20: if(t) { $state = 7; break; } else { $state = 15; break; } case 7: $__2 = inorder(t.left).__traceurIterator__(); $state = 8; break; case 8: if($__2.moveNext()) { $state = 2; break; } else { $state = 5; $finallyFallThrough = 4; break; } case 2: $__1 = $__2.current; $state = 3; break; case 3: $result.current = $__1; $state = 8; return true; case 5: { if($__2.close) $__2.close(); } $state = 6; break; case 4: $result.current = t.label; $state = 10; return true; case 10: $__4 = inorder(t.right).__traceurIterator__(); $state = 19; break; case 19: if($__4.moveNext()) { $state = 13; break; } else { $state = 16; $finallyFallThrough = 15; break; } case 13: $__3 = $__4.current; $state = 14; break; case 14: $result.current = $__3; $state = 19; return true; case 16: { if($__4.close) $__4.close(); } $state = 17; break; case 6: $state = $finallyFallThrough; break; case 17: $state = $finallyFallThrough; break; case 15: $state = 22; case 22: return false; case 21: throw $storedException; default: throw "traceur compiler bug: invalid state in state machine" + $state; } } catch($caughtException) { $storedException = $caughtException; switch($state) { case 8: $state = 5; $finallyFallThrough = 21; break; case 2: $state = 5; $finallyFallThrough = 21; break; case 3: $state = 5; $finallyFallThrough = 21; break; case 19: $state = 16; $finallyFallThrough = 21; break; case 13: $state = 16; $finallyFallThrough = 21; break; case 14: $state = 16; $finallyFallThrough = 21; break; default: throw $storedException; } } }).bind($that) }; return $result; } }; } function make(array) { if(array.length == 1) return new Tree(null, array[0], null); return new Tree(make(array[0]), array[1], make(array[2])); } var tree = make([[['a'], 'b',['c']], 'd',[['e'], 'f',['g']]]); { var $__0 = inorder(tree).__traceurIterator__(); try { while($__0.moveNext()) { try { throw undefined; } catch(node) { node = $__0.current; { console.log(node); } } } } finally { if($__0.close) $__0.close(); } }
      
      



記事の半分のコード-彼にとってすべてが悪いことを示すために残された...



おわりに



オリジナルでは、ジェネレータはFirefoxでのみ使用できます。つまり、ジェネレータは使用できません(ただし、プレイすることはできます)。 Function#bind



Object.defineProperty



を終了しても、いずれの場合もTCはコードを厄介なモンスターに変えてしまうため、その使用は疑わしいです。



それらを正常に適用できる唯一の場所は、将来のSpiderNodeです。 イテレータとジェネレータは非常に便利なものです。 クライアントでは、5年以内にそれらを使用します(すべての既知の理由のため)。 サーバー上で、できればすぐに。



読む



1. MDC- 配列の内包表記

2. ES Harmonyイテレーター

3. ES Harmony Generators

4. MDC- MDCイテレータおよびジェネレータガイド

5. MDC- JavaScript 1.7の新機能

6. traceur-compiler LanguageFeatures



JavaScriptはPythonから1-in-1ジェネレーターとイテレーターを引き継ぎましたが、JavaScriptの例はまだほとんどありません。 Pythonリソースへのリンクをスローします。

7. PEP 380、「 サブジェネレーターに委任するための構文

8. PEP 255、「 単純なジェネレーター

9.プレゼンテーション「 システムプログラマーのためのジェネレータートリックはじめに



提案、希望、批判は大歓迎です!



UPD追加ジェネレーターと共通ループジェネレーターのパフォーマンステスト



All Articles