首頁  >  文章  >  web前端  >  JavaScript中的迭代器與生成器詳解_javascript技巧

JavaScript中的迭代器與生成器詳解_javascript技巧

WBOY
WBOY原創
2016-05-16 16:32:521391瀏覽

處理集合裡的每一項都是非常普通的操作,JavaScript提供了許多方法來迭代一個集合,從簡單的for和for each循環到map(),filter() 和array comprehensions(數組推導式)。在JavaScript 1.7中,迭代器和生成器在JavaScript核心語法中帶來了新的迭代機制,也提供了自訂 for…in 和 for each 迴圈行為的機制。

迭代器

迭代器是一個每次存取集合序列中一個元素的對象,並追蹤該序列中迭代的當前位置。在JavaScript中迭代器是一個對象,這個物件提供了一個 next() 方法,next() 方法傳回序列中的下一個元素。當序列中所有元素都遍歷完成時,此方法拋出 StopIteration 異常。

迭代器物件一旦建立,就可以透過明確的重複呼叫next(),或是使用JavaScript的 for…in 和 for each 迴圈隱式呼叫。

簡單的對物件和陣列進行迭代的迭代器可以使用 Iterator() 被建立:

複製程式碼 程式碼如下:

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

一旦初始化完成,next() 方法可以被呼叫來依序存取物件的鍵值對:

複製程式碼 程式碼如下:

  var pair = it.next(); //鍵值對是["name", "JavaScript"]
    pair = it.next(); //鍵值對是["birthday", 1995]
    pair = it.next(); //一個 `StopIteration` 異常被拋出

for…in 迴圈可以被用來取代明確的呼叫 next() 方法。當 StopIteration 異常被拋出時,循環會自動終止。

複製程式碼 程式碼如下:

 var it = Iterator(lang);
    for (var pair in it)
      print(pair); //每次輸出 it 中的一個 [key, value] 鍵值對

如果你只想迭代物件的 key 值,可以往 Iterator() 函數中傳入第二個參數,值為 true:

複製程式碼 程式碼如下:

  var it = Iterator(lang, true);
    for (var key in it)
      print(key); //每次輸出 key 值

使用 Iterator() 存取物件的一個好處是,被加入到 Object.prototype 的自訂屬性不會被包含在序列物件中。

Iterator() 同樣可以被作用在陣列上:

複製程式碼 程式碼如下:

var langs = ['JavaScript', 'Python', 'Haskell'];
    var it = Iterator(langs);
    for (var pair in it)
      print(pair); //每次迭代輸出 [index, language] 鍵值對

就像遍歷物件一樣,把 true 當做第二個參數傳入遍歷的結果將會是陣列索引:

複製程式碼 程式碼如下:

 var langs = ['JavaScript', 'Python', 'Haskell'];
    var it = Iterator(langs, true);
    for (var i in it)
      print(i); //輸出 0,然後 1,然後 2

使用 let 關鍵字可以在迴圈內部分別分配索引和值給區塊變量,還可以解構賦值(Destructuring Assignment):

複製程式碼 程式碼如下:

 var langs = ['JavaScript', 'Python', 'Haskell'];
    var it = Iterators(langs);
    for (let [i, lang] in it)
      print(i ': ' lang); //輸出 "0: JavaScript" 等

宣告自訂迭代器

一些代表元素集合的物件應該用一種指定的方式來迭代。

1.迭代一個表示範圍(Range)的物件應該一個接一個的回傳這個範圍所包含的數字
2.一個樹的葉子節點可以使用深度優先或廣度優先存取到
3.迭代一個代表資料庫查詢結果的物件應該一行一行的返回,即使整個結果集尚未全部載入到一個單一數組
4.作用在一個無限數學序列(像斐波那契序列)上的迭代器應該在不創建無限長度資料結構的前提下一個接一個的返回結果

JavaScript 允許你寫自訂迭代邏輯的程式碼,並把它作用在一個物件上

我們建立一個簡單的 Range 對象,包含低和高兩個值:

複製程式碼 程式碼如下:

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

現在我們建立一個自訂迭代器,它會傳回一個包含範圍內所有整數的序列。迭代器介面需要我們提供一個 next() 方法用來傳回序列中的下一個元素或是拋出 StopIteration 異常。

複製程式碼 程式碼如下:

 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 ;
    };

我們的 RangeIterator 透過 range 實例來實例化,同時維持一個 current 屬性來追蹤目前序列的位置。

最後,為了讓 RangeIterator 可以和 Range 結合起來,我們需要為 Range 加入一個特殊的 __iterator__ 方法。當我們試圖去迭代一個 Range 時,它將被調用,並且應該返回一個實現了迭代邏輯的 RangeIterator 實例。

複製程式碼 程式碼如下:

Range.prototype.__iterator__ = function(){
      return new RangeIterator(this);
    };

完成我們的自訂迭代器後,我們就可以迭代一個範圍實例:

複製程式碼 程式碼如下:

var range = new Range(3, 5);
    for (var i in range)
      print(i); //輸出 3,然後 4,然後 5

生成器:一種更好的方式來建立迭代器

雖然自訂的迭代器是一種很有用的工具,但是創建它們的時候要仔細規劃,因為需要明確的維護它們的內部狀態。

生成器提供了很強大的功能:它允許你定義一個包含自有迭代演算法的函數, 同時它可以自動維護自己的狀態。

生成器是可以作為迭代器工廠的特殊函數。如果一個函數包含了一個或多個 yield 表達式,那麼就稱它為生成器(譯者註:Node.js 還需要在函數名前加 * 來表示)。

注意:只有 HTML 中被包含在

當一個生成器函數被呼叫時,函數體不會即刻執行,它會傳回一個 generator-iterator 物件。每次呼叫 generator-iterator 的 next() 方法,函數體就會執行到下一個 yield 表達式,然後傳回它的結果。當函數結束或碰到 return 語句,一個 StopIteration 異常會被拋出。

用一個例子來更好的說明:

複製程式碼 程式碼如下:

function simpleGenerator(){
      yield "first";
      yield "second";
      yield "third";
      for (var i = 0; i         yield i;
    }
   
    var g = simpleGenerator();
    print(g.next()); //輸出 "first"
    print(g.next()); //輸出 "second"
    print(g.next()); //輸出 "third"
    print(g.next()); //輸出 0
    print(g.next()); //輸出 1
    print(g.next()); //輸出 2
    print(g.next()); //拋出 StopIteration 異常

產生器函數可以被一個類別直接的當做 __iterator__ 方法使用,在需要自訂迭代器的地方可以有效的減少程式碼量。我們使用生成器重寫 Range :

複製程式碼 程式碼如下:

function Range(low, high){
      this.low = low;
      this.high = high;
    }
    Range.prototype.__iterator__ = function(){
      for (var i = this.low; i         yield i;
    };
    var range = new Range(3, 5);
    for (var i in range)
      print(i); //輸出 3,然後 4,然後 5

不是所有的生成器都會終止,你可以建立一個代表無限序列的生成器。下面的生成器實作一個斐波那契序列,就是每一個元素都是前面兩個的和:

複製程式碼 程式碼如下:

function fibonacci(){
      var fn1 = 1;
      var fn2 = 1;
      while (1) {
        var current = fn2;
        fn2 = fn1;
        fn1 = fn1 current;
        yield current;
      }
    }
   
    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.next()); // 8
    print(sequence.next()); // 13

生成器函數可以帶有參數,並且在第一次呼叫函數時會使用這些參數。生成器可以被終止(引起它拋出 StopIteration 異常)通過使用 return 語句。下面的 fibonacci() 變體帶有一個可選的 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;
      }
    }

生成器高階特性

產生器可以根據需求計算yield回傳值,這使得它可以表示以前昂貴的序列計算需求,甚至是上面所示的無限序列。

除了 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.next());     //8
    print(sequence.next());     //13
    print(sequence.send(true)); //1
    print(sequence.next());     //1
    print(sequence.next());     //2
    print(sequence.next());     //3

注意:有趣的一點是,呼叫 send(undefined) 和呼叫 next() 是完全同等的。不過,當呼叫 send() 方法啟動新的生成器時,除了 undefined 其它的值都會拋出一個 TypeError 例外。

你可以呼叫 throw 方法並且傳遞一個它應該拋出的異常值來強制生成器拋出一個異常。此異常將從當前上下文拋出並暫停生成器,類似當前的 yield 執行,只不過換成了 throw value 語句。

如果在拋出異常的處理過程中沒有遇到 yield ,該異常將會被傳遞直到調用 throw() 方法,並且隨後調用 next() 將會導致 StopIteration 異常被拋出。

生成器擁有一個 close() 方法來強制生成器結束。結束一個生成器會產生以下影響:

1.所有生成器中有效的 finally 字句將會執行
2.如果 finally 字句拋出了除 StopIteration 以外的任何異常,該異常將會被傳遞到 close() 方法的呼叫者
3.生成器會終止

生成器表達式

陣列推導式 的一個明顯缺點是,它們會導致整個陣列在記憶體中構造。當輸入到推導式的本身是個小數組時它的開銷是微不足道的—但是,當輸入數組很大或者創建一個新的昂貴(或者是無限的)數組生成器時就可能出現問題。

產生器允許對序列延遲計算(lazy computation),在需要時按需計算元素。生成器表達式在句法上幾乎和數組推導式相同—它用圓括號來代替方括號(而且用for...in 代替for each...in)—但是它創建一個生成器而不是數組,這樣就可以延遲計算。你可以把它想像成創建生成器的簡短語法。

假設我們有一個迭代器 it 來迭代一個巨大的整數序列。我們需要建立一個新的迭代器來迭代偶數。一個陣列推導式將會在記憶體中建立整個包含所有偶數的陣列:

複製程式碼 程式碼如下:

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

而生成器表達式將會建立一個新的迭代器,並且在需要的時候按需來計算偶數值:

複製程式碼 程式碼如下:

 var it2 = (i * 2 for (i in it));
    print(it2.next());  //it 裡面的第一個偶數
    print(it2.next());  //it 裡面的第二個偶數

當一個生成器被用做函數的參數,圓括號被用做函數調用,意味著最外層的圓括號可以被省略:

複製程式碼 程式碼如下:

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

End.

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn