高階函數(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)注的是模式可以幫助我們完成什么剧董。