JavaScript設(shè)計模式之迭代器模式

迭代器模式是指提供一種方法順序訪問一個聚合對象中的各個元素挤安,而又不需要暴露該對象的內(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)置了迭代器桂肌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末数焊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子崎场,更是在濱河造成了極大的恐慌佩耳,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谭跨,死亡現(xiàn)場離奇詭異干厚,居然都是意外死亡李滴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門蛮瞄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來所坯,“玉大人,你說我怎么就攤上這事挂捅∏壑” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵闲先,是天一觀的道長状土。 經(jīng)常有香客問我,道長伺糠,這世上最難降的妖魔是什么蒙谓? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮训桶,結(jié)果婚禮上累驮,老公的妹妹穿的比我還像新娘。我一直安慰自己舵揭,他們只是感情好谤专,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著琉朽,像睡著了一般毒租。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上箱叁,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天墅垮,我揣著相機與錄音,去河邊找鬼耕漱。 笑死算色,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的螟够。 我是一名探鬼主播灾梦,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妓笙!你這毒婦竟也來了若河?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤寞宫,失蹤者是張志新(化名)和其女友劉穎萧福,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辈赋,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡鲫忍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年膏燕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悟民。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡坝辫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出射亏,到底是詐尸還是另有隱情近忙,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布智润,位于F島的核電站银锻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏做鹰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一鼎姐、第九天 我趴在偏房一處隱蔽的房頂上張望钾麸。 院中可真熱鬧,春花似錦炕桨、人聲如沸饭尝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钥平。三九已至,卻和暖如春姊途,著一層夾襖步出監(jiān)牢的瞬間涉瘾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工捷兰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留立叛,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓贡茅,卻偏偏與公主長得像秘蛇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子顶考,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容