高階函數(shù)--實(shí)現(xiàn)AOP,函數(shù)節(jié)流夯秃,分時(shí)函數(shù)座咆,惰性加載函數(shù)

高階函數(shù)是指至少滿足下列條件之一的函數(shù)痢艺。

  • 函數(shù)可以作為參數(shù)被傳遞;

  • 函數(shù)可以作為返回值輸出介陶。

  • JavaScript語(yǔ)言中的函數(shù)顯然滿足高階函數(shù)的條件堤舒,在實(shí)際開發(fā)中,無(wú)論是將函數(shù)當(dāng)作參數(shù)傳遞哺呜,還是讓函數(shù)的執(zhí)行結(jié)果返回另外一個(gè)函數(shù)舌缤,這兩種情形都有很多應(yīng)用場(chǎng)景,下面就列舉一些高階函數(shù)的應(yīng)用場(chǎng)景某残。

函數(shù)作為參數(shù)傳遞

把函數(shù)當(dāng)作參數(shù)傳遞国撵,這代表我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯,把這部分業(yè)務(wù)邏輯放在函數(shù)參數(shù)中玻墅,這樣一來(lái)可以分離業(yè)務(wù)代碼中變化與不變的部分介牙。其中一個(gè)重要應(yīng)用場(chǎng)景就是常見的回調(diào)函數(shù)。

1. 回調(diào)函數(shù)

在ajax異步請(qǐng)求的應(yīng)用中澳厢,回調(diào)函數(shù)的使用非常頻繁环础。當(dāng)我們想在ajax請(qǐng)求返回之后做一些事情,但又并不知道請(qǐng)求返回的確切時(shí)間時(shí)剩拢,最常見的方案就是把callback函數(shù)當(dāng)作參數(shù)傳入發(fā)起ajax請(qǐng)求的方法中线得,待請(qǐng)求完成之后執(zhí)行callback函數(shù):

var getUserInfo = function( userId, callback ){
     $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){
        if ( typeof callback === 'function' ){
            callback( data );
        }
    });
}

getUserInfo( 13157, function( data ){
    alert ( data.userName );
});

回調(diào)函數(shù)的應(yīng)用不僅只在異步請(qǐng)求中,當(dāng)一個(gè)函數(shù)不適合執(zhí)行一些請(qǐng)求時(shí)徐伐,我們也可以把這些請(qǐng)求封裝成一個(gè)函數(shù)贯钩,并把它作為參數(shù)傳遞給另外一個(gè)函數(shù),“委托”給另外一個(gè)函數(shù)來(lái)執(zhí)行呵晨。

比如魏保,我們想在頁(yè)面中創(chuàng)建100個(gè)div節(jié)點(diǎn),然后把這些div節(jié)點(diǎn)都設(shè)置為隱藏摸屠。下面是一種編寫代碼的方式:

var appendDiv = function(){
    for ( var i = 0; i < 100; i++ ){
        var div = document.createElement( 'div' );
        div.innerHTML = i;
        document.body.appendChild( div );
        div.style.display = 'none';
    }
};

appendDiv();

把div.style.display = 'none'的邏輯硬編碼在appendDiv里顯然是不合理的谓罗,appendDiv未免有點(diǎn)個(gè)性化,成為了一個(gè)難以復(fù)用的函數(shù)季二,并不是每個(gè)人創(chuàng)建了節(jié)點(diǎn)之后就希望它們立刻被隱藏檩咱。

于是我們把div.style.display = 'none'這行代碼抽出來(lái),用回調(diào)函數(shù)的形式傳入appendDiv方法:

var appendDiv = function( callback ){
    for ( var i = 0; i < 100; i++ ){
        var div = document.createElement( 'div' );
        div.innerHTML = i;
        document.body.appendChild( div );
        if ( typeof callback === 'function' ){
            callback( div );
        }
    }
};

appendDiv(function( node ){
    node.style.display = 'none';
});

可以看到胯舷,隱藏節(jié)點(diǎn)的請(qǐng)求實(shí)際上是由客戶發(fā)起的刻蚯,但是客戶并不知道節(jié)點(diǎn)什么時(shí)候會(huì)創(chuàng)建好,于是把隱藏節(jié)點(diǎn)的邏輯放在回調(diào)函數(shù)中桑嘶,“委托”給appendDiv方法炊汹。appendDiv方法當(dāng)然知道節(jié)點(diǎn)什么時(shí)候創(chuàng)建好,所以在節(jié)點(diǎn)創(chuàng)建好的時(shí)候逃顶,appendDiv會(huì)執(zhí)行之前客戶傳入的回調(diào)函數(shù)讨便。

2. Array.prototype.sort

Array.prototype.sort接受一個(gè)函數(shù)當(dāng)作參數(shù)充甚,這個(gè)函數(shù)里面封裝了數(shù)組元素的排序規(guī)則。從Array.prototype.sort的使用可以看到霸褒,我們的目的是對(duì)數(shù)組進(jìn)行排序伴找,這是不變的部分;而使用什么規(guī)則去排序废菱,則是可變的部分技矮。把可變的部分封裝在函數(shù)參數(shù)里,動(dòng)態(tài)傳入Array.prototype.sort殊轴,使Array.prototype.sort方法成為了一個(gè)非常靈活的方法衰倦,代碼如下:

//從小到大排列

[ 1, 4, 3 ].sort( function( a, b ){
    return a - b;
});

// 輸出: [ 1, 3, 4 ]


//從大到小排列

[ 1, 4, 3 ].sort( function( a, b ){
    return b - a;
});

// 輸出: [ 4, 3, 1 ]

函數(shù)作為返回值輸出

相比把函數(shù)當(dāng)作參數(shù)傳遞,函數(shù)當(dāng)作返回值輸出的應(yīng)用場(chǎng)景也許更多旁理,也更能體現(xiàn)函數(shù)式編程的巧妙耿币。讓函數(shù)繼續(xù)返回一個(gè)可執(zhí)行的函數(shù),意味著運(yùn)算過程是可延續(xù)的韧拒。

1. 判斷數(shù)據(jù)的類型

我們來(lái)看看這個(gè)例子,判斷一個(gè)數(shù)據(jù)是否是數(shù)組十性,在以往的實(shí)現(xiàn)中叛溢,可以基于鴨子類型的概念來(lái)判斷,比如判斷這個(gè)數(shù)據(jù)有沒有l(wèi)ength屬性劲适,有沒有sort方法或者slice方法等楷掉。但更好的方式是用Object.prototype.toString來(lái)計(jì)算。Object.prototype.toString.call( obj )返回一個(gè)字符串霞势,比如Object.prototype.toString.call( [1,2,3] )總是返回"[object Array]"烹植,而Object.prototype.toString.call( "str")總是返回"[object String]"。所以我們可以編寫一系列的isType函數(shù)愕贡。代碼如下:

var isString = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object String]';
};

var isArray = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Array]';
};

var isNumber = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Number]';
};

我們發(fā)現(xiàn)草雕,這些函數(shù)的大部分實(shí)現(xiàn)都是相同的,不同的只是Object.prototype.toString.call( obj )返回的字符串固以。為了避免多余的代碼墩虹,我們嘗試把這些字符串作為參數(shù)提前值入isType函數(shù)。代碼如下:

var isType = function( type ){
    return function( obj ){
        return Object.prototype.toString.call( obj ) === '[object '+ type +']';
    }
};

var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );

console.log( isArray( [ 1, 2, 3 ] ) );     // 輸出:true

我們還可以用循環(huán)語(yǔ)句憨琳,來(lái)批量注冊(cè)這些isType函數(shù):

var Type = {};

for ( var i = 0, type; type = [ 'String', 'Array', 'Number' ][ i++ ]; ){
    (function( type ){
        Type[ 'is' + type ] = function( obj ){
            return Object.prototype.toString.call( obj ) === '[object '+ type +']';
           }
       })( type )
};

Type.isArray( [] );     // 輸出:true
Type.isString( "str" );     // 輸出:true

2. getSingle

下面是一個(gè)單例模式的例子诫钓,在第三部分設(shè)計(jì)模式的學(xué)習(xí)中,我們將進(jìn)行更深入的講解篙螟,這里暫且只了解其代碼實(shí)現(xiàn):

var getSingle = function ( fn ) {
    var ret;
    return function () {
        return ret || ( ret = fn.apply( this, arguments ) );
    };
};

這個(gè)高階函數(shù)的例子菌湃,既把函數(shù)當(dāng)作參數(shù)傳遞,又讓函數(shù)執(zhí)行后返回了另外一個(gè)函數(shù)遍略。我們可以看看getSingle函數(shù)的效果:

var getScript = getSingle(function(){
    return document.createElement( 'script' );
});

var script1 = getScript();
var script2 = getScript();

alert ( script1 === script2 );    // 輸出:true

高階函數(shù)實(shí)現(xiàn)AOP

AOP(面向切面編程)的主要作用是把一些跟核心業(yè)務(wù)邏輯模塊無(wú)關(guān)的功能抽離出來(lái)惧所,這些跟業(yè)務(wù)邏輯無(wú)關(guān)的功能通常包括日志統(tǒng)計(jì)骤坐、安全控制、異常處理等纯路。把這些功能抽離出來(lái)之后或油,再通過“動(dòng)態(tài)織入”的方式摻入業(yè)務(wù)邏輯模塊中。這樣做的好處首先是可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性驰唬,其次是可以很方便地復(fù)用日志統(tǒng)計(jì)等功能模塊顶岸。

在Java語(yǔ)言中,可以通過反射和動(dòng)態(tài)代理機(jī)制來(lái)實(shí)現(xiàn)AOP技術(shù)叫编。而在JavaScript這種動(dòng)態(tài)語(yǔ)言中辖佣,AOP的實(shí)現(xiàn)更加簡(jiǎn)單,這是JavaScript與生俱來(lái)的能力搓逾。

通常卷谈,在JavaScript中實(shí)現(xiàn)AOP,都是指把一個(gè)函數(shù)“動(dòng)態(tài)織入”到另外一個(gè)函數(shù)之中霞篡,具體的實(shí)現(xiàn)技術(shù)有很多世蔗,這里我們通過擴(kuò)展Function.prototype來(lái)做到這一點(diǎn)。代碼如下:

Function.prototype.before = function( beforefn ){
    var __self = this;    // 保存原函數(shù)的引用
    return function(){    // 返回包含了原函數(shù)和新函數(shù)的"代理"函數(shù)
        beforefn.apply( this, arguments );     // 執(zhí)行新函數(shù)朗兵,修正this
        return __self.apply( this, arguments );    // 執(zhí)行原函數(shù)
    }
};

Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};

var func = function(){
    console.log( 2 );
};

func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log( 3 );
});

func();

我們把負(fù)責(zé)打印數(shù)字1和打印數(shù)字3的兩個(gè)函數(shù)通過AOP的方式動(dòng)態(tài)植入func函數(shù)污淋。通過執(zhí)行上面的代碼,我們看到控制臺(tái)順利地返回了執(zhí)行結(jié)果1余掖、2寸爆、3。

這種使用AOP的方式來(lái)給函數(shù)添加職責(zé)盐欺,也是JavaScript語(yǔ)言中一種非常特別和巧妙的裝飾者模式實(shí)現(xiàn)赁豆。這種裝飾者模式在實(shí)際開發(fā)中非常有用。

高階函數(shù)的其他應(yīng)用

1. currying

首先我們討論的是函數(shù)柯里化(function currying)冗美。currying的概念最早由俄國(guó)數(shù)學(xué)家Moses Sch?nfinkel發(fā)明魔种,而后由著名的數(shù)理邏輯學(xué)家Haskell Curry將其豐富和發(fā)展,currying由此得名墩衙。

currying又稱部分求值务嫡。一個(gè)currying的函數(shù)首先會(huì)接受一些參數(shù),接受了這些參數(shù)之后漆改,該函數(shù)并不會(huì)立即求值心铃,而是繼續(xù)返回另外一個(gè)函數(shù),剛才傳入的參數(shù)在函數(shù)形成的閉包中被保存起來(lái)挫剑。待到函數(shù)被真正需要求值的時(shí)候去扣,之前傳入的所有參數(shù)都會(huì)被一次性用于求值。

從字面上理解currying并不太容易,我們來(lái)看下面的例子愉棱。

假設(shè)我們要編寫一個(gè)計(jì)算每月開銷的函數(shù)唆铐。在每天結(jié)束之前,我們都要記錄今天花掉了多少錢奔滑。代碼如下:

var monthlyCost = 0;

var cost = function( money ){
    monthlyCost += money;
};

cost( 100 );    // 第1天開銷
cost( 200 );    // 第2天開銷
cost( 300 );    // 第3天開銷
//cost( 700 );    // 第30天開銷

alert ( monthlyCost );      // 輸出:600

通過這段代碼可以看到艾岂,每天結(jié)束后我們都會(huì)記錄并計(jì)算到今天為止花掉的錢。但我們其實(shí)并不太關(guān)心每天花掉了多少錢朋其,而只想知道到月底的時(shí)候會(huì)花掉多少錢王浴。也就是說,實(shí)際上只需要在月底計(jì)算一次梅猿。

如果在每個(gè)月的前29天氓辣,我們都只是保存好當(dāng)天的開銷,直到第30天才進(jìn)行求值計(jì)算袱蚓,這樣就達(dá)到了我們的要求钞啸。雖然下面的cost函數(shù)還不是一個(gè)currying函數(shù)的完整實(shí)現(xiàn),但有助于我們了解其思想:

var cost = (function(){
    var args = [];

    return function(){
        if ( arguments.length === 0 ){
            var money = 0;
            for ( var i = 0, l = args.length; i < l; i++ ){
                money += args[ i ];
            }
            return money;
        }else{
            [].push.apply( args, arguments );
        }
    }

})();

cost( 100 );    // 未真正求值
cost( 200 );    // 未真正求值
cost( 300 );    // 未真正求值

console.log( cost() );       // 求值并輸出:600

接下來(lái)我們編寫一個(gè)通用的function currying(){}喇潘,function currying(){}接受一個(gè)參數(shù)体斩,即將要被currying的函數(shù)。在這個(gè)例子里颖低,這個(gè)函數(shù)的作用遍歷本月每天的開銷并求出它們的總和硕勿。代碼如下:

var currying = function( fn ){
    var args = [];

    return function(){
        if ( arguments.length === 0 ){
            return fn.apply( this, args );
        }else{
            [].push.apply( args, arguments );
            return arguments.callee;
        }
    }

};

var cost = (function(){
    var money = 0;

    return function(){
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            money += arguments[ i ];
        }
        return money;
    }

})();

var cost = currying( cost );    // 轉(zhuǎn)化成currying函數(shù)

cost( 100 );    // 未真正求值
cost( 200 );    // 未真正求值
cost( 300 );    // 未真正求值

alert ( cost() );     // 求值并輸出:600

至此,我們完成了一個(gè)currying函數(shù)的編寫枫甲。當(dāng)調(diào)用cost()時(shí),如果明確地帶上了一些參數(shù)扼褪,表示此時(shí)并不進(jìn)行真正的求值計(jì)算想幻,而是把這些參數(shù)保存起來(lái),此時(shí)讓cost函數(shù)返回另外一個(gè)函數(shù)话浇。只有當(dāng)我們以不帶參數(shù)的形式執(zhí)行cost()時(shí)脏毯,才利用前面保存的所有參數(shù),真正開始進(jìn)行求值計(jì)算幔崖。

2. uncurrying

在JavaScript中食店,當(dāng)我們調(diào)用對(duì)象的某個(gè)方法時(shí),其實(shí)不用去關(guān)心該對(duì)象原本是否被設(shè)計(jì)為擁有這個(gè)方法赏寇,這是動(dòng)態(tài)類型語(yǔ)言的特點(diǎn)吉嫩,也是常說的鴨子類型思想。

同理嗅定,一個(gè)對(duì)象也未必只能使用它自身的方法自娩,那么有什么辦法可以讓對(duì)象去借用一個(gè)原本不屬于它的方法呢?

答案對(duì)于我們來(lái)說很簡(jiǎn)單渠退,call和apply都可以完成這個(gè)需求:

var obj1 = {
    name: 'sven'
};

var obj2 = {
    getName: function(){
        return this.name;
    }
};

console.log( obj2.getName.call( obj1 ) );     // 輸出:sven

我們常常讓類數(shù)組對(duì)象去借用Array.prototype的方法忙迁,這是call和apply最常見的應(yīng)用場(chǎng)景之一:

(function(){
    Array.prototype.push.call( arguments, 4 );    // arguments借用Array.prototype.push方法
    console.log( arguments );      // 輸出:[1, 2, 3, 4]
})( 1, 2, 3 );

在我們的預(yù)期中脐彩,Array.prototype上的方法原本只能用來(lái)操作array對(duì)象。但用call和apply可以把任意對(duì)象當(dāng)作this傳入某個(gè)方法姊扔,這樣一來(lái)惠奸,方法中用到this的地方就不再局限于原來(lái)規(guī)定的對(duì)象,而是加以泛化并得到更廣的適用性恰梢。

那么有沒有辦法把泛化this的過程提取出來(lái)呢佛南?下面講述的uncurrying如何解決這個(gè)問題的。以下代碼是uncurrying的實(shí)現(xiàn)方式之一:

Function.prototype.uncurrying = function () {
    var self = this;
    return function() {
        var obj = Array.prototype.shift.call( arguments );
        return self.apply( obj, arguments );
    };
};

在講解這段代碼的實(shí)現(xiàn)原理之前删豺,我們先來(lái)瞧瞧它有什么作用共虑。

在類數(shù)組對(duì)象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call這句代碼轉(zhuǎn)換為一個(gè)通用的push函數(shù):

var push = Array.prototype.push.uncurrying();

(function(){
    push( arguments, 4 );
    console.log( arguments );     // 輸出:[1, 2, 3, 4]
})( 1, 2, 3 );

通過uncurrying的方式呀页,Array.prototype.push.call變成了一個(gè)通用的push函數(shù)妈拌。這樣一來(lái),push函數(shù)的作用就跟Array.prototype.push一樣了蓬蝶,同樣不僅僅局限于只能操作array對(duì)象尘分。而對(duì)于使用者而言,調(diào)用push函數(shù)的方式也顯得更加簡(jiǎn)潔和意圖明了丸氛。

我們還可以一次性地把Array.prototype上的方法“復(fù)制”到array對(duì)象上培愁,同樣這些方法可操作的對(duì)象也不僅僅只是array對(duì)象:

for ( var i = 0, fn, ary = [ 'push', 'shift', 'forEach' ]; fn = ary[ i++ ]; ){
    Array[ fn ] = Array.prototype[ fn ].uncurrying();
};

var obj = {
    "length": 3,
    "0": 1,
    "1": 2,
    "2": 3
};

Array.push( obj, 4 );     // 向?qū)ο笾刑砑右粋€(gè)元素
console.log( obj.length );    // 輸出:4

var first = Array.shift( obj );    // 截取第一個(gè)元素
console.log( first );     // 輸出:1
console.log( obj );    // 輸出:{0: 2, 1: 3, 2: 4, length: 3}

Array.forEach( obj, function( i, n ){
    console.log( n );      // 分別輸出:0, 1, 2
});

甚至Function.prototype.call和Function.prototype.apply本身也可以被uncurrying,不過這沒有實(shí)用價(jià)值缓窜,只是使得對(duì)函數(shù)的調(diào)用看起來(lái)更像JavaScript語(yǔ)言的前身Scheme:

var call = Function.prototype.call.uncurrying();
var fn = function( name ){
    console.log( name );
};
call( fn, window, 'sven' );     // 輸出:sven

var apply = Function.prototype.apply.uncurrying();
var fn = function( name ){
    console.log( this.name );     // 輸出:"sven"
    console.log( arguments );     // 輸出: [1, 2, 3]
};
apply( fn, { name: 'sven' }, [ 1, 2, 3 ] );

目前我們已經(jīng)給出了Function.prototype.uncurrying的一種實(shí)現(xiàn)《ㄐ現(xiàn)在來(lái)分析調(diào)用Array.prototype.push.uncurrying()這句代碼時(shí)發(fā)生了什么事情:

Function.prototype.uncurrying = function () {
    var self = this;     // self此時(shí)是Array.prototype.push
    return function() {
        var obj = Array.prototype.shift.call( arguments );
        // obj是{
        //    "length": 1,
        //    "0": 1
        // }
        // arguments對(duì)象的第一個(gè)元素被截去,剩下[2]
        return self.apply( obj, arguments );
        // 相當(dāng)于Array.prototype.push.apply( obj, 2 )
    };
};

var push = Array.prototype.push.uncurrying();
var obj = {
    "length": 1,
    "0": 1
};

push( obj, 2 );
console.log( obj );     // 輸出:{0: 1, 1: 2, length: 2}

除了剛剛提供的代碼實(shí)現(xiàn)禾锤,下面的代碼是uncurrying的另外一種實(shí)現(xiàn)方式:

Function.prototype.uncurrying = function(){
    var self = this;
    return function(){
        return Function.prototype.call.apply( self, arguments );
    }
};

3. 函數(shù)節(jié)流

JavaScript中的函數(shù)大多數(shù)情況下都是由用戶主動(dòng)調(diào)用觸發(fā)的私股,除非是函數(shù)本身的實(shí)現(xiàn)不合理,否則我們一般不會(huì)遇到跟性能相關(guān)的問題恩掷。但在一些少數(shù)情況下倡鲸,函數(shù)的觸發(fā)不是由用戶直接控制的。在這些場(chǎng)景下黄娘,函數(shù)有可能被非常頻繁地調(diào)用峭状,而造成大的性能問題。下面將列舉一些這樣的場(chǎng)景逼争。

(1) 函數(shù)被頻繁調(diào)用的場(chǎng)景

window.onresize事件优床。我們給window對(duì)象綁定了resize事件,當(dāng)瀏覽器窗口大小被拖動(dòng)而改變的時(shí)候誓焦,這個(gè)事件觸發(fā)的頻率非常之高羔巢。如果我們?cè)趙indow.onresize事件函數(shù)里有一些跟DOM節(jié)點(diǎn)相關(guān)的操作,而跟DOM節(jié)點(diǎn)相關(guān)的操作往往是非常消耗性能的,這時(shí)候?yàn)g覽器可能就會(huì)吃不消而造成卡頓現(xiàn)象竿秆。

mousemove事件启摄。同樣,如果我們給一個(gè)div節(jié)點(diǎn)綁定了拖曳事件(主要是mousemove)幽钢,當(dāng)div節(jié)點(diǎn)被拖動(dòng)的時(shí)候歉备,也會(huì)頻繁地觸發(fā)該拖曳事件函數(shù)。

上傳進(jìn)度匪燕。微云的上傳功能使用了公司提供的一個(gè)瀏覽器插件蕾羊。該瀏覽器插件在真正開始上傳文件之前,會(huì)對(duì)文件進(jìn)行掃描并隨時(shí)通知JavaScript函數(shù)帽驯,以便在頁(yè)面中顯示當(dāng)前的掃描進(jìn)度龟再。但該插件通知的頻率非常之高,大約一秒鐘10次尼变,很顯然我們?cè)陧?yè)面中不需要如此頻繁地去提示用戶利凑。

(2) 函數(shù)節(jié)流的原理

我們整理上面提到的三個(gè)場(chǎng)景,發(fā)現(xiàn)它們面臨的共同問題是函數(shù)被觸發(fā)的頻率太高嫌术。

比如我們?cè)趙indow.onresize事件中要打印當(dāng)前的瀏覽器窗口大小哀澈,在我們通過拖曳來(lái)改變窗口大小的時(shí)候,打印窗口大小的工作1秒鐘進(jìn)行了10次度气。而我們實(shí)際上只需要2次或者3次割按。這就需要我們按時(shí)間段來(lái)忽略掉一些事件請(qǐng)求,比如確保在500ms內(nèi)只打印一次磷籍。很顯然适荣,我們可以借助setTimeout來(lái)完成這件事情。

(3) 函數(shù)節(jié)流的代碼實(shí)現(xiàn)

關(guān)于函數(shù)節(jié)流的代碼實(shí)現(xiàn)有許多種院领,下面的throttle 函數(shù)的原理是束凑,將即將被執(zhí)行的函數(shù)用setTimeout延遲一段時(shí)間執(zhí)行。如果該次延遲執(zhí)行還沒有完成栅盲,則忽略接下來(lái)調(diào)用該函數(shù)的請(qǐng)求。throttle函數(shù)接受2個(gè)參數(shù)废恋,第一個(gè)參數(shù)為需要被延遲執(zhí)行的函數(shù)谈秫,第二個(gè)參數(shù)為延遲執(zhí)行的時(shí)間。具體實(shí)現(xiàn)代碼如下:

var throttle = function ( fn, interval ) {

    var __self = fn,    // 保存需要被延遲執(zhí)行的函數(shù)引用
        timer,      // 定時(shí)器
        firstTime = true;    // 是否是第一次調(diào)用

    return function () {
        var args = arguments,
            __me = this;

        if ( firstTime ) {    // 如果是第一次調(diào)用鱼鼓,不需延遲執(zhí)行
            __self.apply(__me, args);
            return firstTime = false;
        }

        if ( timer ) {    // 如果定時(shí)器還在拟烫,說明前一次延遲執(zhí)行還沒有完成
            return false;
        }

        timer = setTimeout(function () {  // 延遲一段時(shí)間執(zhí)行
            clearTimeout(timer);
            timer = null;
            __self.apply(__me, args);

        }, interval || 500 );

    };

};

window.onresize = throttle(function(){
    console.log( 1 );
}, 500 );

4. 分時(shí)函數(shù)

在前面關(guān)于函數(shù)節(jié)流的討論中,我們提供了一種限制函數(shù)被頻繁調(diào)用的解決方案迄本。下面我們將遇到另外一個(gè)問題硕淑,某些函數(shù)確實(shí)是用戶主動(dòng)調(diào)用的,但因?yàn)橐恍┛陀^的原因,這些函數(shù)會(huì)嚴(yán)重地影響頁(yè)面性能置媳。

一個(gè)例子是創(chuàng)建WebQQ的QQ好友列表于樟。列表中通常會(huì)有成百上千個(gè)好友,如果一個(gè)好友用一個(gè)節(jié)點(diǎn)來(lái)表示拇囊,當(dāng)我們?cè)陧?yè)面中渲染這個(gè)列表的時(shí)候迂曲,可能要一次性往頁(yè)面中創(chuàng)建成百上千個(gè)節(jié)點(diǎn)。

在短時(shí)間內(nèi)往頁(yè)面中大量添加DOM節(jié)點(diǎn)顯然也會(huì)讓瀏覽器吃不消寥袭,我們看到的結(jié)果往往就是瀏覽器的卡頓甚至假死路捧。代碼如下:

var ary = [];

for ( var i = 1; i <= 1000; i++ ){
    ary.push( i );     // 假設(shè)ary裝載了1000個(gè)好友的數(shù)據(jù)
};

var renderFriendList = function( data ){
    for ( var i = 0, l = data.length; i < l; i++ ){
        var div = document.createElement( 'div' );
        div.innerHTML = i;
        document.body.appendChild( div );
    }
};

renderFriendList( ary );

這個(gè)問題的解決方案之一是下面的timeChunk函數(shù),timeChunk 函數(shù)讓創(chuàng)建節(jié)點(diǎn)的工作分批進(jìn)行传黄,比如把1秒鐘創(chuàng)建1000個(gè)節(jié)點(diǎn)杰扫,改為每隔200毫秒創(chuàng)建8個(gè)節(jié)點(diǎn)。

timeChunk函數(shù)接受3個(gè)參數(shù)膘掰,第1個(gè)參數(shù)是創(chuàng)建節(jié)點(diǎn)時(shí)需要用到的數(shù)據(jù)章姓,第2個(gè)參數(shù)是封裝了創(chuàng)建節(jié)點(diǎn)邏輯的函數(shù),第3個(gè)參數(shù)表示每一批創(chuàng)建的節(jié)點(diǎn)數(shù)量炭序。代碼如下:

var timeChunk = function( ary, fn, count ){

    var obj,
        t;

    var len = ary.length;

    var start = function(){
        for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
            var obj = ary.shift();
            fn( obj );
        }
     };

     return function(){
        t = setInterval(function(){
          if ( ary.length === 0 ){  // 如果全部節(jié)點(diǎn)都已經(jīng)被創(chuàng)建好
              return clearInterval( t );
          }
          start();
        }, 200 );    // 分批執(zhí)行的時(shí)間間隔啤覆,也可以用參數(shù)的形式傳入

    };

};

最后我們進(jìn)行一些小測(cè)試,假設(shè)我們有1000個(gè)好友的數(shù)據(jù)惭聂,我們利用timeChunk函數(shù)窗声,每一批只往頁(yè)面中創(chuàng)建8個(gè)節(jié)點(diǎn):

var ary = [];

 for ( var i = 1; i <= 1000; i++ ){
     ary.push( i );
 };

 var renderFriendList = timeChunk( ary, function( n ){
     var div = document.createElement( 'div' );
     div.innerHTML = n;
     document.body.appendChild( div );
 }, 8 );

 renderFriendList();

5. 惰性加載函數(shù)

在Web開發(fā)中,因?yàn)闉g覽器之間的實(shí)現(xiàn)差異辜纲,一些嗅探工作總是不可避免笨觅。比如我們需要一個(gè)在各個(gè)瀏覽器中能夠通用的事件綁定函數(shù)addEvent,常見的寫法如下:

var addEvent = function( elem, type, handler ){
    if ( window.addEventListener ){
       return elem.addEventListener( type, handler, false );
      }
      if ( window.attachEvent ){
          return elem.attachEvent( 'on' + type, handler );
      }
};

這個(gè)函數(shù)的缺點(diǎn)是耕腾,當(dāng)它每次被調(diào)用的時(shí)候都會(huì)執(zhí)行里面的if條件分支见剩,雖然執(zhí)行這些if分支的開銷不算大,但也許有一些方法可以讓程序避免這些重復(fù)的執(zhí)行過程扫俺。

第二種方案是這樣苍苞,我們把嗅探瀏覽器的操作提前到代碼加載的時(shí)候,在代碼加載的時(shí)候就立刻進(jìn)行一次判斷狼纬,以便讓addEvent返回一個(gè)包裹了正確邏輯的函數(shù)羹呵。代碼如下:

var addEvent = (function(){
    if ( window.addEventListener ){
        return function( elem, type, handler ){
            elem.addEventListener( type, handler, false );
        }
    }
    if ( window.attachEvent ){
        return function( elem, type, handler ){
            elem.attachEvent( 'on' + type, handler );
        }
    }
})();

目前的addEvent函數(shù)依然有個(gè)缺點(diǎn),也許我們從頭到尾都沒有使用過addEvent函數(shù)疗琉,這樣看來(lái)冈欢,前一次的瀏覽器嗅探就是完全多余的操作,而且這也會(huì)稍稍延長(zhǎng)頁(yè)面ready的時(shí)間盈简。

第三種方案即是我們將要討論的惰性載入函數(shù)方案凑耻。此時(shí)addEvent依然被聲明為一個(gè)普通函數(shù)太示,在函數(shù)里依然有一些分支判斷。但是在第一次進(jìn)入條件分支之后香浩,在函數(shù)內(nèi)部會(huì)重寫這個(gè)函數(shù)类缤,重寫之后的函數(shù)就是我們期望的addEvent函數(shù),在下一次進(jìn)入addEvent函數(shù)的時(shí)候弃衍,addEvent函數(shù)里不再存在條件分支語(yǔ)句:

<html>
    <body>
        <div id="div1">點(diǎn)我綁定事件</div>
    <script>

    var addEvent = function( elem, type, handler ){
        if ( window.addEventListener ){
           addEvent = function( elem, type, handler ){
               elem.addEventListener( type, handler, false );
           }
        }else if ( window.attachEvent ){
            addEvent = function( elem, type, handler ){
                elem.attachEvent( 'on' + type, handler );
            }
        }

        addEvent( elem, type, handler );
    };

      var div = document.getElementById( 'div1' );

      addEvent( div, 'click', function(){
          alert (1);
      });

      addEvent( div, 'click', function(){
          alert (2);
      });

    </script>
    </body>
</html>

小結(jié)

在JavaScript開發(fā)中呀非,閉包和高階函數(shù)的應(yīng)用極多。就設(shè)計(jì)模式而言镜盯,因?yàn)镴avaScript這門語(yǔ)言的自身特點(diǎn)岸裙,許多設(shè)計(jì)模式在JavaScript之中的實(shí)現(xiàn)跟在一些傳統(tǒng)面向?qū)ο笳Z(yǔ)言中的實(shí)現(xiàn)相差很大。在JavaScript中速缆,很多設(shè)計(jì)模式都是通過閉包和高階函數(shù)實(shí)現(xiàn)的降允。這并不奇怪,相對(duì)于模式的實(shí)現(xiàn)過程艺糜,我們更關(guān)注的是模式可以幫助我們完成什么剧董。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市破停,隨后出現(xiàn)的幾起案子翅楼,更是在濱河造成了極大的恐慌,老刑警劉巖真慢,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅臊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡黑界,警方通過查閱死者的電腦和手機(jī)管嬉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)朗鸠,“玉大人蚯撩,你說我怎么就攤上這事≈蛘迹” “怎么了胎挎?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)忆家。 經(jīng)常有香客問我犹菇,道長(zhǎng),這世上最難降的妖魔是什么弦赖? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮浦辨,結(jié)果婚禮上蹬竖,老公的妹妹穿的比我還像新娘沼沈。我一直安慰自己,他們只是感情好币厕,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布列另。 她就那樣靜靜地躺著,像睡著了一般旦装。 火紅的嫁衣襯著肌膚如雪页衙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天阴绢,我揣著相機(jī)與錄音店乐,去河邊找鬼。 笑死呻袭,一個(gè)胖子當(dāng)著我的面吹牛眨八,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播左电,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼廉侧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了篓足?” 一聲冷哼從身側(cè)響起段誊,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栈拖,沒想到半個(gè)月后连舍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辱魁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年烟瞧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片染簇。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡参滴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锻弓,到底是詐尸還是另有隱情砾赔,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布青灼,位于F島的核電站暴心,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏杂拨。R本人自食惡果不足惜专普,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弹沽。 院中可真熱鬧檀夹,春花似錦筋粗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蚌堵,卻和暖如春买决,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吼畏。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工督赤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宫仗。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓够挂,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親藕夫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孽糖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

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