迭代器和產生器
迭代器和產生器
處理集合中的每一項是很常見的操作。JavaScript 提供了許多迭代整個集合的方式,從簡單的 for
和 for each
循環到 map()
、filter()
以及 陣列的簡約式。迭代器和產生器是在 JavaScript 1.7 引入的,帶來在核心語言中直接迭代的觀念,並提供自訂 for...in
和 for each
循環的行為的機制。
yield
關鍵字只能在 HTML 裡包有 <script type="application/javascript;version=1.7">
區塊(或更高的版本)的代碼區塊中使用。 XUL Script 標記可以存取這些功能,無須這個特別的區塊。迭代器
迭代器(Iterator)是一種知道如何從集合裡每次以同樣的方式存取項目的物件。並保持對自己序列內部的目前位置的追蹤。在 JavaScript 中,迭代器是一種提供有能夠返回序列中的下一項的 next()
方法的物件。這個方法可以在序列用盡時,選擇性的出現 StopIteration
例外。
迭代器物件一經建立以後,可以明確的反覆呼叫 next()
來使用,或隱含的使用 JavaScript 的 for...in
和 for each
結構。
可以使用 Iterator()
函數來為物件和陣列建立簡單的迭代器︰
var lang = { name: 'JavaScript', birthYear: 1995 }; var it = Iterator(lang);
一經初始化以後,就可以呼叫 next()
方法依序從物件中存取鍵值對(key-value pair)︰
var pair = it.next(); // 鍵值對是 ["name", "JavaScript"] pair = it.next(); // 鍵值對是 ["birthYear", 1995] pair = it.next(); // 拋出 StopIteration 例外
可以使用 for...in
循環來取代直接呼叫 next()
方法。循環會在 StopIteration
例外出現的時候自動終止。
var it = Iterator(lang); for (var pair in it) print(pair); // 依序輸出每一個 [key, value] 對
如果我們只想要迭代物件的鍵(key),我們可以把第二個參數 true
傳給 Iterator()
函數︰
var it = Iterator(lang, true); for (var key in it) print(key); // 依序輸出每一個鍵
使用 Iterator()
存取物件內容的其中一個好處是已經加入到 Object.prototype
的自訂屬性不會被包含在序列裡。
Iterator()
也可以和陣列一起使用︰
var langs = ['JavaScript', 'Python', 'C++']; var it = Iterator(langs); for (var pair in it) print(pair); // 依序輸出每一個 [index, language] 對
如同物件一般,傳入 true
作為第二個參數,將導致迭代出存在於陣列裡的索引︰
var langs = ['JavaScript', 'Python', 'C++']; var it = Iterator(langs, true); for (var i in it) print(i); // 輸出 0,然後是 1,然後是 2
他也可以把 for 循環內部使用的 let
關鍵字的索引和值兩者,代入給區塊作用域的變數,並分割代入︰
var langs = ['JavaScript', 'Python', 'C++']; var it = Iterator(langs); for (let [i, lang] in it) print(i + ': ' + lang); // 輸出 "0: JavaScript" 等等。
自訂的迭代器的定義
有一些物件所表示的集合項,應該以特定的方式來迭代。
- 迭代物件的某一範圍,應該一個接著一個返回範圍內的數字。
- 可以使用深度優先或廣度優先的遍歷法來遊覽樹裡的葉項。
- 表示來自資料庫的查詢結果的物件的迭代,應該一行接著一行返回,即使整份結果尚未完全載入到單一的陣列。
- 無限長的數學序列(如費伯納契數列)的迭代,應該要能夠一個接著一個返回,而無須建立無限長的資料結構。
JavaScript 可讓你編寫表示自訂迭代器邏輯的代碼,並把他連結到物件上。
我們將會建立簡單的 Range
物件,這個物件存放了 low 和 high 值。
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; var current = this.current; this.current += 1; return current; };
我們的 RangeIterator
就是有範圍限制的實體的實際例子,並維護他自己的 current
屬性用以追蹤他沿著序列已走了多遠。
最後,把我們的 RangeIterator
和 Range
物件連繫在一起,我們需要加入特別的 __iterator__
方法給 Range
。這個方法會在當我們試圖迭代 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
產生器︰建構迭代器的較佳方式
儘管自訂的迭代器是很好用的工具,但在建立時的程式設計必須要很謹慎,因為需要明確的維護他們的內部狀態。產生器(Generator)提供另一種更強大的選擇︰可讓你編寫能夠維護自身狀態的單一函數來定義迭代器的演算法。
產生器是一種特殊類型的函數,他的運作方式類似迭代器的生產廠房。只要函數內含有一個以上的 yield
語句,就會變成產生器。
當產生器函數被呼叫的時候,並不會立即執行函數的本體;取而代之的是,他會返回產生器迭代器(generator-iterator)物件。每次呼叫產生器迭代器的 next()
方法,就會執行函數本體直到下一個 yield
語句,並返回他的結果。如果執行到函數的末端或到達 return
語句,就會拋出 StopIteration
例外。
配合例子是最佳的說明︰
function simpleGenerator() { yield "first"; yield "second"; yield "third"; for (var i = 0; i < 3; 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 <= this.high; 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
產生器函數可以接受參數,這個參數是用來約束第一次被呼叫的函數。產生器可以使用 return
語句來終止(並導致 StopIteration
例外的出現)。下面的 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; } }
高階的產生器
產生器會計算出要求他們產生的值,這可讓產生器更有效率的表示需要耗費大量計算的序列,甚至是上面示範的無窮數列。
除了 next()
方法以外,產生器迭代器物件還有 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()
。然而,使用除了 undefined 以外的任意值啟動新生的產生器,在呼叫 send()
的時候,將會引起 TypeError
例外。你可以藉由呼叫產生器的 throw()
方法,並傳入他應該拋出的例外值,強迫產生器拋出例外。例外將會從目前被中止的產生器的位置拋出例外,就如同目前被中止的 yield
被替換成 throw value
語句一樣。
如果在拋出例外的處理期間沒有遇到 yield,然後例外將會不斷傳播直到呼叫 throw()
,且隨後呼叫 next()
將導致 StopIteration
被拋出。
產生器有可以強迫關閉他自己的 close()
方法。關閉產生器的效果是︰
- 執行所有在產生器函數裡的
finally
子句。 - 如果
finally
子句拋出除了StopIteration
以外的任何例外,例外會被傳播到close()
方法的呼叫者。 - 產生器終止。
產生器的表達式
陣列簡約式主要的缺點是他們會造成在記憶體中建構出完整的新陣列。如果輸入的簡約式本身是小型的陣列其開銷還不明顯 - 但如果輸入的是大型的陣列或耗費資源(甚至是無限大)的產生器,新陣列的建立就會產生問題。
產生器能夠延後計算他們要求的所需計算的項的序列。產生器表達式在語法上幾乎等同於 陣列的簡約式 - 他們使用圓括弧取代方括號 - 但不是建構陣列,他們建立可以延後執行的產生器。你可以把他們想成建立產生器的簡寫語法。
假設我們有一個迭代器 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));