迭代器模式是指提供一種方法順序訪問一個聚合對象中的各個元素挤安,而又不需要暴露該對象的內(nèi)部表示喜最。迭代器模式可以把迭代的過程從業(yè)務(wù)邏輯中分離出來澜倦,在使用迭代器模式之后,即使不關(guān)心對象的內(nèi)部構(gòu)造篡殷,也可以按順序訪問其中的每個元素钝吮。
目前,恐怕只有在一些“古董級”的語言中才會為實現(xiàn)一個迭代器模式而煩惱板辽,現(xiàn)在流行的大部分語言如Java奇瘦、Ruby等都已經(jīng)有了內(nèi)置的迭代器實現(xiàn),許多瀏覽器也支持JavaScript的Array.prototype.forEach劲弦。
jQuery中的迭代器
迭代器模式無非就是循環(huán)訪問聚合對象中的各個元素耳标。比如jQuery中的$.each函數(shù),其中回調(diào)函數(shù)中的參數(shù)i為當前索引邑跪,n為當前元素次坡,代碼如下:
$.each( [1, 2, 3], function( i, n ){
console.log( '當前下標為: '+ i );
console.log( '當前值為:' + n );
});
實現(xiàn)自己的迭代器
現(xiàn)在我們來自己實現(xiàn)一個each函數(shù),each函數(shù)接受2個參數(shù)画畅,第一個為被循環(huán)的數(shù)組砸琅,第二個為循環(huán)中的每一步后將被觸發(fā)的回調(diào)函數(shù):
var each = function( ary, callback ){
for ( var i = 0, l = ary.length; i < l; i++ ){
callback.call( ary[i], i, ary[ i ] ); // 把下標和元素當作參數(shù)傳給callback函數(shù)
}
};
each( [ 1, 2, 3 ], function( i, n ){
alert ( [ i, n ] );
});
內(nèi)部迭代器和外部迭代器
迭代器可以分為內(nèi)部迭代器和外部迭代器,它們有各自的適用場景轴踱。這里我們將分別討論這兩種迭代器症脂。
1.內(nèi)部迭代器
我們剛剛編寫的each函數(shù)屬于內(nèi)部迭代器,each函數(shù)的內(nèi)部已經(jīng)定義好了迭代規(guī)則淫僻,它完全接手整個迭代過程诱篷,外部只需要一次初始調(diào)用。
內(nèi)部迭代器在調(diào)用的時候非常方便雳灵,外界不用關(guān)心迭代器內(nèi)部的實現(xiàn)兴蒸,跟迭代器的交互也僅僅是一次初始調(diào)用,但這也剛好是內(nèi)部迭代器的缺點细办。由于內(nèi)部迭代器的迭代規(guī)則已經(jīng)被提前規(guī)定,上面的each函數(shù)就無法同時迭代2個數(shù)組了。
比如現(xiàn)在有個需求笑撞,要判斷2個數(shù)組里元素的值是否完全相等岛啸, 如果不改寫each函數(shù)本身的代碼,我們能夠入手的地方似乎只剩下each的回調(diào)函數(shù)了茴肥,代碼如下:
var compare = function( ary1, ary2 ){
if ( ary1.length !== ary2.length ){
throw new Error ( 'ary1和ary2不相等' );
}
each( ary1, function( i, n ){
if ( n !== ary2[ i ] ){
throw new Error ( 'ary1和ary2不相等' );
}
});
alert ( 'ary1和ary2相等' );
};
compare( [ 1, 2, 3 ], [ 1, 2, 4 ] ); // throw new Error ( 'ary1和ary2不相等' );
說實話坚踩,這個compare函數(shù)一點都算不上好看,我們目前能夠順利完成需求瓤狐,還要感謝在JavaScript里可以把函數(shù)當作參數(shù)傳遞的特性瞬铸,但在其他語言中未必就能如此幸運。
在一些沒有閉包的語言中础锐,內(nèi)部迭代器本身的實現(xiàn)也相當復(fù)雜嗓节。比如C語言中的內(nèi)部迭代器是用函數(shù)指針來實現(xiàn)的,循環(huán)處理所需要的數(shù)據(jù)都要以參數(shù)的形式明確地從外面?zhèn)鬟f進去皆警。
2.外部迭代器
外部迭代器必須顯式地請求迭代下一個元素拦宣。
外部迭代器增加了一些調(diào)用的復(fù)雜度,但相對也增強了迭代器的靈活性信姓,我們可以手工控制迭代的過程或者順序鸵隧。
下面這個外部迭代器的實現(xiàn)來自《松本行弘的程序世界》第4章,原例用Ruby寫成意推,這里我們翻譯成JavaScript:
var Iterator = function( obj ){
var current = 0;
var next = function(){
current += 1;
};
var isDone = function(){
return current >= obj.length;
};
var getCurrItem = function(){
return obj[ current ];
};
return {
next: next,
isDone: isDone,
getCurrItem: getCurrItem
}
};
再看看如何改寫compare函數(shù):
var compare = function( iterator1, iterator2 ){
while( !iterator1.isDone() && !iterator2.isDone() ){
if ( iterator1.getCurrItem() !== iterator2.getCurrItem() ){
throw new Error ( 'iterator1和iterator2不相等' );
}
iterator1.next();
iterator2.next();
}
alert ( 'iterator1和iterator2相等' );
}
var iterator1 = Iterator( [ 1, 2, 3 ] );
var iterator2 = Iterator( [ 1, 2, 3 ] );
compare( iterator1, iterator2 ); // 輸出:iterator1和iterator2相等
外部迭代器雖然調(diào)用方式相對復(fù)雜豆瘫,但它的適用面更廣,也能滿足更多變的需求菊值。內(nèi)部迭代器和外部迭代器在實際生產(chǎn)中沒有優(yōu)劣之分外驱,究竟使用哪個要根據(jù)需求場景而定。
迭代類數(shù)組對象和字面量對象
迭代器模式不僅可以迭代數(shù)組俊性,還可以迭代一些類數(shù)組的對象略步。比如arguments、{"0":'a',"1":'b'}等定页。 通過上面的代碼可以觀察到趟薄,無論是內(nèi)部迭代器還是外部迭代器,只要被迭代的聚合對象擁有l(wèi)ength屬性而且可以用下標訪問典徊,那它就可以被迭代杭煎。
在JavaScript中,for in語句可以用來迭代普通字面量對象的屬性卒落。jQuery中提供了$.each`函數(shù)來封裝各種迭代行為:
$.each = function( obj, callback ) {
var value,
i = 0,
length = obj.length,
isArray = isArraylike( obj );
if ( isArray ) { // 迭代類數(shù)組
for ( ; i < length; i++ ) {
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
} else {
for ( i in obj ) { // 迭代object對象
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
}
return obj;
};
倒序迭代器
由于GoF中對迭代器模式的定義非常松散羡铲,所以我們可以有多種多樣的迭代器實現(xiàn)±鼙希總的來說也切, 迭代器模式提供了循環(huán)訪問一個聚合對象中每個元素的方法扑媚,但它沒有規(guī)定我們以順序、倒序還是中序來循環(huán)遍歷聚合對象雷恃。
下面我們分分鐘實現(xiàn)一個倒序訪問的迭代器:
var reverseEach = function( ary, callback ){
for ( var l = ary.length - 1; l >= 0; l-- ){
callback( l, ary[ l ] );
}
};
reverseEach( [ 0, 1, 2 ], function( i, n ){
console.log( n ); // 分別輸出:2, 1 ,0
});
中止迭代器
迭代器可以像普通for循環(huán)中的break一樣疆股,提供一種跳出循環(huán)的方法。在上面jQuery的each函數(shù)里有這樣一句:
if ( value === false ) {
break;
}
這句代碼的意思是倒槐,約定如果回調(diào)函數(shù)的執(zhí)行結(jié)果返回false旬痹,則提前終止循環(huán)。下面我們把之前的each函數(shù)改寫一下:
var each = function( ary, callback ){
for ( var i = 0, l = ary.length; i < l; i++ ){
if ( callback( i, ary[ i ] ) === false ){ // callback的執(zhí)行結(jié)果返回false讨越,提前終止迭代
break;
}
}
};
each( [ 1, 2, 3, 4, 5 ], function( i, n ){
if ( n > 3 ){ // n大于3的時候終止循環(huán)
return false;
}
console.log( n ); // 分別輸出:1, 2, 3
});
7.7 迭代器模式的應(yīng)用舉例
下面這段代碼两残,它的目的是根據(jù)不同的瀏覽器獲取相應(yīng)的上傳組件對象:
var getUploadObj = function(){
try{
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE上傳控件
}catch(e){
if ( supportFlash() ){ // supportFlash函數(shù)未提供
var str = '<object type="application/x-shockwave-flash"></object>';
return $( str ).appendTo( $('body') );
}else{
var str = '<input name="file" type="file"/>'; // 表單上傳
return $( str ).appendTo( $('body') );
}
}
};
在不同的瀏覽器環(huán)境下,選擇的上傳方式是不一樣的把跨。因為使用瀏覽器的上傳控件進行上傳速度快人弓,可以暫停和續(xù)傳,所以我們首先會優(yōu)先使用控件上傳节猿。如果瀏覽器沒有安裝上傳控件票从,則使用Flash上傳, 如果連Flash也沒安裝滨嘱,那就只好使用瀏覽器原生的表單上傳了峰鄙。
看看上面的代碼,為了得到一個upload對象太雨,這個getUploadObj函數(shù)里面充斥了try吟榴,catch以及if條件分支。缺點是顯而易見的囊扳。第一是很難閱讀吩翻,第二是嚴重違反開閉原則。 在開發(fā)和調(diào)試過程中锥咸,我們需要來回切換不同的上傳方式狭瞎,每次改動都相當痛苦。后來我們還增加支持了一些另外的上傳方式搏予,比如熊锭,HTML5上傳,這時候唯一的辦法是繼續(xù)往getUploadObj函數(shù)里增加條件分支雪侥。
現(xiàn)在來梳理一下問題碗殷,目前一共有3種可能的上傳方式,我們不知道目前正在使用的瀏覽器支持哪幾種速缨。就好比我們有一個鑰匙串锌妻,其中共有3把鑰匙,我們想打開一扇門但是不知道該使用哪把鑰匙旬牲,于是從第一把鑰匙開始仿粹,迭代鑰匙串進行嘗試搁吓,直到找到了正確的鑰匙為止。
同樣吭历,我們把每種獲取upload對象的方法都封裝在各自的函數(shù)里擎浴,然后使用一個迭代器,迭代獲取這些upload對象毒涧,直到獲取到一個可用的為止:
var getActiveUploadObj = function(){
try{
return new ActiveXObject( "TXFTNActiveX.FTNUpload" ); // IE上傳控件
}catch(e){
return false;
}
};
var getFlashUploadObj = function(){
if ( supportFlash() ){ // supportFlash函數(shù)未提供
var str = '<object type="application/x-shockwave-flash"></object>';
return $( str ).appendTo( $('body') );
}
return false;
};
var getFormUpladObj = function(){
var str = '<input name="file" type="file" class="ui-file"/>'; // 表單上傳
return $( str ).appendTo( $('body') );
};
在getActiveUploadObj、getFlashUploadObj贝室、getFormUpladObj這3個函數(shù)中都有同一個約定:如果該函數(shù)里面的upload對象是可用的契讲,則讓函數(shù)返回該對象,反之返回false滑频,提示迭代器繼續(xù)往后面進行迭代捡偏。
所以我們的迭代器只需進行下面這幾步工作。
提供一個可以被迭代的方法峡迷,使得getActiveUploadObj银伟,getFlashUploadObj以及getFlashUploadObj 依照優(yōu)先級被循環(huán)迭代。
如果正在被迭代的函數(shù)返回一個對象绘搞,則表示找到了正確的upload對象彤避,反之如果該函數(shù)返回false,則讓迭代器繼續(xù)工作夯辖。
迭代器代碼如下:
var iteratorUploadObj = function(){
for ( var i = 0, fn; fn = arguments[ i++ ]; ){
var uploadObj = fn();
if ( uploadObj !== false ){
return uploadObj;
}
}
};
var uploadObj = iteratorUploadObj( getActiveUploadObj, getFlashUploadObj, getFormUpladObj );
重構(gòu)代碼之后琉预,我們可以看到,獲取不同上傳對象的方法被隔離在各自的函數(shù)里互不干擾蒿褂,try圆米、catch和if分支不再糾纏在一起,使得我們可以很方便地的維護和擴展代碼啄栓。比如娄帖,后來我們又給上傳項目增加了Webkit控件上傳和HTML5上傳,我們要做的僅僅是下面一些工作昙楚。
增加分別獲取Webkit控件上傳對象和HTML5上傳對象的函數(shù):
var getWebkitUploadObj = function(){
// 具體代碼略
};
var getHtml5UploadObj = function(){
// 具體代碼略
};
依照優(yōu)先級把它們添加進迭代器:
var uploadObj = iteratorUploadObj( getActiveUploadObj, getWebkitUploadObj,
getFlashUploadObj, getHtml5UploadObj, getFormUpladObj );
小結(jié)
迭代器模式是一種相對簡單的模式近速,簡單到很多時候我們都不認為它是一種設(shè)計模式。目前的絕大部分語言都內(nèi)置了迭代器桂肌。