處理集合里的每一項是一個非常普通的操作,JavaScript提供了許多方法來迭代一個集合,從簡單的for和for each循環(huán)到 map(),filter() 和 array comprehensions(數(shù)組推導(dǎo)式)。在JavaScript 1.7中,迭代器和生成器在JavaScript核心語法中帶來了新的迭代機制,而且還提供了定制 for…in 和 for each 循環(huán)行為的機制。
迭代器
迭代器是一個每次訪問集合序列中一個元素的對象,并跟蹤該序列中迭代的當(dāng)前位置。在JavaScript中迭代器是一個對象,這個對象提供了一個 next() 方法,next() 方法返回序列中的下一個元素。當(dāng)序列中所有元素都遍歷完成時,該方法拋出 StopIteration 異常。
迭代器對象一旦被建立,就可以通過顯式的重復(fù)調(diào)用next(),或者使用JavaScript的 for…in 和 for each 循環(huán)隱式調(diào)用。
簡單的對對象和數(shù)組進行迭代的迭代器可以使用 Iterator() 被創(chuàng)建:
代碼如下:
var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);
一旦初始化完成,next() 方法可以被調(diào)用來依次訪問對象的鍵值對:
代碼如下:
var pair = it.next(); //鍵值對是["name", "JavaScript"]
pair = it.next(); //鍵值對是["birthday", 1995]
pair = it.next(); //一個 `StopIteration` 異常被拋出
for…in 循環(huán)可以被用來替換顯式的調(diào)用 next() 方法。當(dāng) StopIteration 異常被拋出時,循環(huán)會自動終止。
代碼如下:
var it = Iterator(lang);
for (var pair in it)
print(pair); //每次輸出 it 中的一個 [key, value] 鍵值對
如果你只想迭代對象的 key 值,可以往 Iterator() 函數(shù)中傳入第二個參數(shù),值為 true:
代碼如下:
var it = Iterator(lang, true);
for (var key in it)
print(key); //每次輸出 key 值
使用 Iterator() 訪問對象的一個好處是,被添加到 Object.prototype 的自定義屬性不會被包含在序列對象中。
Iterator() 同樣可以被作用在數(shù)組上:
代碼如下:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterator(langs);
for (var pair in it)
print(pair); //每次迭代輸出 [index, language] 鍵值對
就像遍歷對象一樣,把 true 當(dāng)做第二個參數(shù)傳入遍歷的結(jié)果將會是數(shù)組索引:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterator(langs, true);
for (var i in it)
print(i); //輸出 0,然后是 1,然后是 2
使用 let 關(guān)鍵字可以在循環(huán)內(nèi)部分別分配索引和值給塊變量,還可以解構(gòu)賦值(Destructuring Assignment):
代碼如下:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterators(langs);
for (let [i, lang] in it)
print(i + ': ' + lang); //輸出 "0: JavaScript" 等
聲明自定義迭代器
一些代表元素集合的對象應(yīng)該用一種指定的方式來迭代。
1.迭代一個表示范圍(Range)的對象應(yīng)該一個接一個的返回這個范圍包含的數(shù)字
2.一個樹的葉子節(jié)點可以使用深度優(yōu)先或者廣度優(yōu)先訪問到
3.迭代一個代表數(shù)據(jù)庫查詢結(jié)果的對象應(yīng)該一行一行的返回,即使整個結(jié)果集尚未全部加載到一個單一數(shù)組
4.作用在一個無限數(shù)學(xué)序列(像斐波那契序列)上的迭代器應(yīng)該在不創(chuàng)建無限長度數(shù)據(jù)結(jié)構(gòu)的前提下一個接一個的返回結(jié)果
JavaScript 允許你寫自定義迭代邏輯的代碼,并把它作用在一個對象上
我們創(chuàng)建一個簡單的 Range 對象,包含低和高兩個值:
代碼如下:
function Range(low, high){
this.low = low;
this.high = high;
}
現(xiàn)在我們創(chuàng)建一個自定義迭代器,它返回一個包含范圍內(nèi)所有整數(shù)的序列。迭代器接口需要我們提供一個 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 屬性來跟蹤當(dāng)前序列的位置。
最后,為了讓 RangeIterator 可以和 Range 結(jié)合起來,我們需要為 Range 添加一個特殊的 __iterator__ 方法。當(dāng)我們試圖去迭代一個 Range 時,它將被調(diào)用,而且應(yīng)該返回一個實現(xiàn)了迭代邏輯的 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
生成器:一種更好的方式來構(gòu)建迭代器
雖然自定義的迭代器是一種很有用的工具,但是創(chuàng)建它們的時候要仔細(xì)規(guī)劃,因為需要顯式的維護它們的內(nèi)部狀態(tài)。
生成器提供了很強大的功能:它允許你定義一個包含自有迭代算法的函數(shù), 同時它可以自動維護自己的狀態(tài)。
生成器是可以作為迭代器工廠的特殊函數(shù)。如果一個函數(shù)包含了一個或多個 yield 表達式,那么就稱它為生成器(譯者注:Node.js 還需要在函數(shù)名前加 * 來表示)。
注意:只有 HTML 中被包含在 <script type="application/javascript;version=1.7"> (或者更高版本)中的代碼塊才可以使用 yield 關(guān)鍵字。XUL (XML User Interface Language) 腳本標(biāo)簽不需要指定這個特殊的代碼塊也可以訪問這些特性。
當(dāng)一個生成器函數(shù)被調(diào)用時,函數(shù)體不會即刻執(zhí)行,它會返回一個 generator-iterator 對象。每次調(diào)用 generator-iterator 的 next() 方法,函數(shù)體就會執(zhí)行到下一個 yield 表達式,然后返回它的結(jié)果。當(dāng)函數(shù)結(jié)束或者碰到 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 異常
生成器函數(shù)可以被一個類直接的當(dāng)做 __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
不是所有的生成器都會終止,你可以創(chuàng)建一個代表無限序列的生成器。下面的生成器實現(xiàn)一個斐波那契序列,就是每一個元素都是前面兩個的和:
代碼如下:
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
生成器函數(shù)可以帶有參數(shù),并且會在第一次調(diào)用函數(shù)時使用這些參數(shù)。生成器可以被終止(引起它拋出 StopIteration 異常)通過使用 return 語句。下面的 fibonacci() 變體帶有一個可選的 limit 參數(shù),當(dāng)條件被觸發(fā)時終止函數(shù)。
代碼如下:
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;
}
}
生成器高級特性
生成器可以根據(jù)需求計算yield返回值,這使得它可以表示以前昂貴的序列計算需求,甚至是上面所示的無限序列。
除了 next() 方法,generator-iterator 對象還有一個 send() 方法,該方法可以修改生成器的內(nèi)部狀態(tài)。傳給 send() 的值將會被當(dāng)做最后一個 yield 表達式的結(jié)果,并且會暫停生成器。在你使用 send() 方法傳一個指定值前,你必須至少調(diào)用一次 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
注意:有意思的一點是,調(diào)用 send(undefined) 和調(diào)用 next() 是完全同等的。不過,當(dāng)調(diào)用 send() 方法啟動一個新的生成器時,除了 undefined 其它的值都會拋出一個 TypeError 異常。
你可以調(diào)用 throw 方法并且傳遞一個它應(yīng)該拋出的異常值來強制生成器拋出一個異常。此異常將從當(dāng)前上下文拋出并暫停生成器,類似當(dāng)前的 yield 執(zhí)行,只不過換成了 throw value 語句。
如果在拋出異常的處理過程中沒有遇到 yield ,該異常將會被傳遞直到調(diào)用 throw() 方法,并且隨后調(diào)用 next() 將會導(dǎo)致 StopIteration 異常被拋出。
生成器擁有一個 close() 方法來強制生成器結(jié)束。結(jié)束一個生成器會產(chǎn)生如下影響:
1.所有生成器中有效的 finally 字句將會執(zhí)行
2.如果 finally 字句拋出了除 StopIteration 以外的任何異常,該異常將會被傳遞到 close() 方法的調(diào)用者
3.生成器會終止
生成器表達式
數(shù)組推導(dǎo)式 的一個明顯缺點是,它們會導(dǎo)致整個數(shù)組在內(nèi)存中構(gòu)造。當(dāng)輸入到推導(dǎo)式的本身是個小數(shù)組時它的開銷是微不足道的—但是,當(dāng)輸入數(shù)組很大或者創(chuàng)建一個新的昂貴(或者是無限的)數(shù)組生成器時就可能出現(xiàn)問題。
生成器允許對序列延遲計算(lazy computation),在需要時按需計算元素。生成器表達式在句法上幾乎和數(shù)組推導(dǎo)式相同—它用圓括號來代替方括號(而且用 for...in 代替 for each...in)—但是它創(chuàng)建一個生成器而不是數(shù)組,這樣就可以延遲計算。你可以把它想象成創(chuàng)建生成器的簡短語法。
假設(shè)我們有一個迭代器 it 來迭代一個巨大的整數(shù)序列。我們需要創(chuàng)建一個新的迭代器來迭代偶數(shù)。一個數(shù)組推導(dǎo)式將會在內(nèi)存中創(chuàng)建整個包含所有偶數(shù)的數(shù)組:
代碼如下:
var doubles = [i * 2 for (i in it)];
而生成器表達式將會創(chuàng)建一個新的迭代器,并且在需要的時候按需來計算偶數(shù)值:
代碼如下:
var it2 = (i * 2 for (i in it));
print(it2.next()); //it 里面的第一個偶數(shù)
print(it2.next()); //it 里面的第二個偶數(shù)
當(dāng)一個生成器被用做函數(shù)的參數(shù),圓括號被用做函數(shù)調(diào)用,意味著最外層的圓括號可以被省略:
代碼如下:
var result = doSomething(i * 2 for (i in it));
更多信息請查看IT技術(shù)專欄