- JavaScript設(shè)計模式與開發(fā)實踐
- 曾探
- 594字
- 2020-01-10 15:38:14
3.2 高階函數(shù)
高階函數(shù)是指至少滿足下列條件之一的函數(shù)。
? 函數(shù)可以作為參數(shù)被傳遞;
? 函數(shù)可以作為返回值輸出。
JavaScript語言中的函數(shù)顯然滿足高階函數(shù)的條件,在實際開發(fā)中,無論是將函數(shù)當(dāng)作參數(shù)傳遞,還是讓函數(shù)的執(zhí)行結(jié)果返回另外一個函數(shù),這兩種情形都有很多應(yīng)用場景,下面就列舉一些高階函數(shù)的應(yīng)用場景。
3.2.1 函數(shù)作為參數(shù)傳遞
把函數(shù)當(dāng)作參數(shù)傳遞,這代表我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯,把這部分業(yè)務(wù)邏輯放在函數(shù)參數(shù)中,這樣一來可以分離業(yè)務(wù)代碼中變化與不變的部分。其中一個重要應(yīng)用場景就是常見的回調(diào)函數(shù)。
1.回調(diào)函數(shù)
在ajax異步請求的應(yīng)用中,回調(diào)函數(shù)的使用非常頻繁。當(dāng)我們想在ajax請求返回之后做一些事情,但又并不知道請求返回的確切時間時,最常見的方案就是把callback函數(shù)當(dāng)作參數(shù)傳入發(fā)起ajax請求的方法中,待請求完成之后執(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)用不僅只在異步請求中,當(dāng)一個函數(shù)不適合執(zhí)行一些請求時,我們也可以把這些請求封裝成一個函數(shù),并把它作為參數(shù)傳遞給另外一個函數(shù),“委托”給另外一個函數(shù)來執(zhí)行。
比如,我們想在頁面中創(chuàng)建100個div節(jié)點,然后把這些div節(jié)點都設(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未免有點個性化,成為了一個難以復(fù)用的函數(shù),并不是每個人創(chuàng)建了節(jié)點之后就希望它們立刻被隱藏。
于是我們把div.style.display = 'none’這行代碼抽出來,用回調(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é)點的請求實際上是由客戶發(fā)起的,但是客戶并不知道節(jié)點什么時候會創(chuàng)建好,于是把隱藏節(jié)點的邏輯放在回調(diào)函數(shù)中,“委托”給appendDiv方法。appendDiv方法當(dāng)然知道節(jié)點什么時候創(chuàng)建好,所以在節(jié)點創(chuàng)建好的時候,appendDiv會執(zhí)行之前客戶傳入的回調(diào)函數(shù)。
2. Array.prototype.sort
Array.prototype.sort接受一個函數(shù)當(dāng)作參數(shù),這個函數(shù)里面封裝了數(shù)組元素的排序規(guī)則。從Array.prototype.sort的使用可以看到,我們的目的是對數(shù)組進行排序,這是不變的部分;而使用什么規(guī)則去排序,則是可變的部分。把可變的部分封裝在函數(shù)參數(shù)里,動態(tài)傳入Array.prototype.sort,使Array.prototype.sort方法成為了一個非常靈活的方法,代碼如下:
//從小到大排列 [ 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 ]
3.2.2 函數(shù)作為返回值輸出
相比把函數(shù)當(dāng)作參數(shù)傳遞,函數(shù)當(dāng)作返回值輸出的應(yīng)用場景也許更多,也更能體現(xiàn)函數(shù)式編程的巧妙。讓函數(shù)繼續(xù)返回一個可執(zhí)行的函數(shù),意味著運算過程是可延續(xù)的。
1.判斷數(shù)據(jù)的類型
我們來看看這個例子,判斷一個數(shù)據(jù)是否是數(shù)組,在以往的實現(xiàn)中,可以基于鴨子類型的概念來判斷,比如判斷這個數(shù)據(jù)有沒有l(wèi)ength屬性,有沒有sort方法或者slice方法等。但更好的方式是用Object.prototype.toString來計算。Object.prototype.toString.call( obj )返回一個字符串,比如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ù)的大部分實現(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)語句,來批量注冊這些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
下面是一個單例模式的例子,在第三部分設(shè)計模式的學(xué)習(xí)中,我們將進行更深入的講解,這里暫且只了解其代碼實現(xiàn):
var getSingle = function ( fn ) { var ret; return function () { return ret || ( ret = fn.apply( this, arguments ) ); }; };
這個高階函數(shù)的例子,既把函數(shù)當(dāng)作參數(shù)傳遞,又讓函數(shù)執(zhí)行后返回了另外一個函數(shù)。我們可以看看getSingle函數(shù)的效果:
var getScript = getSingle(function(){ return document.createElement( 'script' ); }); var script1 = getScript(); var script2 = getScript(); alert ( script1 === script2 ); // 輸出:true
3.2.3 高階函數(shù)實現(xiàn)AOP
AOP(面向切面編程)的主要作用是把一些跟核心業(yè)務(wù)邏輯模塊無關(guān)的功能抽離出來,這些跟業(yè)務(wù)邏輯無關(guān)的功能通常包括日志統(tǒng)計、安全控制、異常處理等。把這些功能抽離出來之后,再通過“動態(tài)織入”的方式摻入業(yè)務(wù)邏輯模塊中。這樣做的好處首先是可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性,其次是可以很方便地復(fù)用日志統(tǒng)計等功能模塊。
在Java語言中,可以通過反射和動態(tài)代理機制來實現(xiàn)AOP技術(shù)。而在JavaScript這種動態(tài)語言中,AOP的實現(xiàn)更加簡單,這是JavaScript與生俱來的能力。
通常,在JavaScript中實現(xiàn)AOP,都是指把一個函數(shù)“動態(tài)織入”到另外一個函數(shù)之中,具體的實現(xiàn)技術(shù)有很多,本節(jié)我們通過擴展Function.prototype來做到這一點。代碼如下:
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();
我們把負責(zé)打印數(shù)字1和打印數(shù)字3的兩個函數(shù)通過AOP的方式動態(tài)植入func函數(shù)。通過執(zhí)行上面的代碼,我們看到控制臺順利地返回了執(zhí)行結(jié)果1、2、3。
這種使用AOP的方式來給函數(shù)添加職責(zé),也是JavaScript語言中一種非常特別和巧妙的裝飾者模式實現(xiàn)。這種裝飾者模式在實際開發(fā)中非常有用,我們將在第15章進行詳細的講解。有興趣的讀者可以提前翻閱第15章進行了解。
3.2.4 高階函數(shù)的其他應(yīng)用
前面我們已經(jīng)學(xué)習(xí)過高階函數(shù),本節(jié)我們再挑選一些常見的高階函數(shù)應(yīng)用進行介紹。
1. currying
首先我們討論的是函數(shù)柯里化(function currying)。currying的概念最早由俄國數(shù)學(xué)家Moses Sch?nfinkel發(fā)明,而后由著名的數(shù)理邏輯學(xué)家Haskell Curry將其豐富和發(fā)展,currying由此得名。
currying又稱部分求值。一個currying的函數(shù)首先會接受一些參數(shù),接受了這些參數(shù)之后,該函數(shù)并不會立即求值,而是繼續(xù)返回另外一個函數(shù),剛才傳入的參數(shù)在函數(shù)形成的閉包中被保存起來。待到函數(shù)被真正需要求值的時候,之前傳入的所有參數(shù)都會被一次性用于求值。
從字面上理解currying并不太容易,我們來看下面的例子。
假設(shè)我們要編寫一個計算每月開銷的函數(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é)束后我們都會記錄并計算到今天為止花掉的錢。但我們其實并不太關(guān)心每天花掉了多少錢,而只想知道到月底的時候會花掉多少錢。也就是說,實際上只需要在月底計算一次。
如果在每個月的前29天,我們都只是保存好當(dāng)天的開銷,直到第30天才進行求值計算,這樣就達到了我們的要求。雖然下面的cost函數(shù)還不是一個currying函數(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
接下來我們編寫一個通用的function currying(){}, function currying(){}接受一個參數(shù),即將要被currying的函數(shù)。在這個例子里,這個函數(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
至此,我們完成了一個currying函數(shù)的編寫。當(dāng)調(diào)用cost()時,如果明確地帶上了一些參數(shù),表示此時并不進行真正的求值計算,而是把這些參數(shù)保存起來,此時讓cost函數(shù)返回另外一個函數(shù)。只有當(dāng)我們以不帶參數(shù)的形式執(zhí)行cost()時,才利用前面保存的所有參數(shù),真正開始進行求值計算。
2. uncurrying
在JavaScript中,當(dāng)我們調(diào)用對象的某個方法時,其實不用去關(guān)心該對象原本是否被設(shè)計為擁有這個方法,這是動態(tài)類型語言的特點,也是常說的鴨子類型思想。
同理,一個對象也未必只能使用它自身的方法,那么有什么辦法可以讓對象去借用一個原本不屬于它的方法呢?
答案對于我們來說很簡單,call和apply都可以完成這個需求:
var obj1 = { name: 'sven' }; var obj2 = { getName: function(){ return this.name; } }; console.log( obj2.getName.call( obj1 ) ); // 輸出:sven
我們常常讓類數(shù)組對象去借用Array.prototype的方法,這是call和apply最常見的應(yī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上的方法原本只能用來操作array對象。但用call和apply可以把任意對象當(dāng)作this傳入某個方法,這樣一來,方法中用到this的地方就不再局限于原來規(guī)定的對象,而是加以泛化并得到更廣的適用性。
Array.prototype上的方法可以操作任何對象的原理可參閱2.2節(jié)。
那么有沒有辦法把泛化this的過程提取出來呢?本小節(jié)講述的uncurrying就是用來解決這個問題的。uncurrying的話題來自JavaScript之父Brendan Eich在2011年發(fā)表的一篇Twitter。以下代碼是uncurrying的實現(xiàn)方式之一:
Function.prototype.uncurrying = function () { var self = this; return function() { var obj = Array.prototype.shift.call( arguments ); return self.apply( obj, arguments ); }; };
在講解這段代碼的實現(xiàn)原理之前,我們先來瞧瞧它有什么作用。
在類數(shù)組對象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call這句代碼轉(zhuǎn)換為一個通用的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變成了一個通用的push函數(shù)。這樣一來,push函數(shù)的作用就跟Array.prototype.push一樣了,同樣不僅僅局限于只能操作array對象。而對于使用者而言,調(diào)用push函數(shù)的方式也顯得更加簡潔和意圖明了。
我們還可以一次性地把Array.prototype上的方法“復(fù)制”到array對象上,同樣這些方法可操作的對象也不僅僅只是array對象:
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ū)ο笾刑砑右粋€元素 console.log( obj.length ); // 輸出:4 var first = Array.shift( obj ); // 截取第一個元素 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ù)的調(diào)用看起來更像JavaScript語言的前身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的一種實現(xiàn)。現(xiàn)在來分析調(diào)用Array.prototype.push.uncurrying()這句代碼時發(fā)生了什么事情:
Function.prototype.uncurrying = function () { var self = this; // self此時是Array.prototype.push return function() { var obj = Array.prototype.shift.call( arguments ); // obj是{ // "length": 1, // "0": 1 // } // arguments對象的第一個元素被截去,剩下[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}
除了剛剛提供的代碼實現(xiàn),下面的代碼是uncurrying的另外一種實現(xiàn)方式:
Function.prototype.uncurrying = function(){ var self = this; return function(){ return Function.prototype.call.apply( self, arguments ); } };
3.函數(shù)節(jié)流
JavaScript中的函數(shù)大多數(shù)情況下都是由用戶主動調(diào)用觸發(fā)的,除非是函數(shù)本身的實現(xiàn)不合理,否則我們一般不會遇到跟性能相關(guān)的問題。但在一些少數(shù)情況下,函數(shù)的觸發(fā)不是由用戶直接控制的。在這些場景下,函數(shù)有可能被非常頻繁地調(diào)用,而造成大的性能問題。下面將列舉一些這樣的場景。
(1) 函數(shù)被頻繁調(diào)用的場景
? window.onresize事件。我們給window對象綁定了resize事件,當(dāng)瀏覽器窗口大小被拖動而改變的時候,這個事件觸發(fā)的頻率非常之高。如果我們在window.onresize事件函數(shù)里有一些跟DOM節(jié)點相關(guān)的操作,而跟DOM節(jié)點相關(guān)的操作往往是非常消耗性能的,這時候瀏覽器可能就會吃不消而造成卡頓現(xiàn)象。
? mousemove事件。同樣,如果我們給一個div節(jié)點綁定了拖曳事件(主要是mousemove),當(dāng)div節(jié)點被拖動的時候,也會頻繁地觸發(fā)該拖曳事件函數(shù)。
? 上傳進度。微云的上傳功能使用了公司提供的一個瀏覽器插件。該瀏覽器插件在真正開始上傳文件之前,會對文件進行掃描并隨時通知JavaScript函數(shù),以便在頁面中顯示當(dāng)前的掃描進度。但該插件通知的頻率非常之高,大約一秒鐘10次,很顯然我們在頁面中不需要如此頻繁地去提示用戶。
(2) 函數(shù)節(jié)流的原理
我們整理上面提到的三個場景,發(fā)現(xiàn)它們面臨的共同問題是函數(shù)被觸發(fā)的頻率太高。
比如我們在window.onresize事件中要打印當(dāng)前的瀏覽器窗口大小,在我們通過拖曳來改變窗口大小的時候,打印窗口大小的工作1秒鐘進行了10次。而我們實際上只需要2次或者3次。這就需要我們按時間段來忽略掉一些事件請求,比如確保在500ms內(nèi)只打印一次。很顯然,我們可以借助setTimeout來完成這件事情。
(3) 函數(shù)節(jié)流的代碼實現(xiàn)
關(guān)于函數(shù)節(jié)流的代碼實現(xiàn)有許多種,下面的throttle函數(shù)的原理是,將即將被執(zhí)行的函數(shù)用setTimeout延遲一段時間執(zhí)行。如果該次延遲執(zhí)行還沒有完成,則忽略接下來調(diào)用該函數(shù)的請求。throttle函數(shù)接受2個參數(shù),第一個參數(shù)為需要被延遲執(zhí)行的函數(shù),第二個參數(shù)為延遲執(zhí)行的時間。具體實現(xiàn)代碼如下:
var throttle = function ( fn, interval ) { var __self = fn, // 保存需要被延遲執(zhí)行的函數(shù)引用 timer, // 定時器 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 ) { // 如果定時器還在,說明前一次延遲執(zhí)行還沒有完成 return false; } timer = setTimeout(function () { // 延遲一段時間執(zhí)行 clearTimeout(timer); timer = null; __self.apply(__me, args); }, interval || 500 ); }; }; window.onresize = throttle(function(){ console.log( 1 ); }, 500 );
4.分時函數(shù)
在前面關(guān)于函數(shù)節(jié)流的討論中,我們提供了一種限制函數(shù)被頻繁調(diào)用的解決方案。下面我們將遇到另外一個問題,某些函數(shù)確實是用戶主動調(diào)用的,但因為一些客觀的原因,這些函數(shù)會嚴(yán)重地影響頁面性能。
一個例子是創(chuàng)建WebQQ的QQ好友列表。列表中通常會有成百上千個好友,如果一個好友用一個節(jié)點來表示,當(dāng)我們在頁面中渲染這個列表的時候,可能要一次性往頁面中創(chuàng)建成百上千個節(jié)點。
在短時間內(nèi)往頁面中大量添加DOM節(jié)點顯然也會讓瀏覽器吃不消,我們看到的結(jié)果往往就是瀏覽器的卡頓甚至假死。代碼如下:
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); // 假設(shè)ary裝載了1000個好友的數(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 );
這個問題的解決方案之一是下面的timeChunk函數(shù),timeChunk函數(shù)讓創(chuàng)建節(jié)點的工作分批進行,比如把1秒鐘創(chuàng)建1000個節(jié)點,改為每隔200毫秒創(chuàng)建8個節(jié)點。
timeChunk函數(shù)接受3個參數(shù),第1個參數(shù)是創(chuàng)建節(jié)點時需要用到的數(shù)據(jù),第2個參數(shù)是封裝了創(chuàng)建節(jié)點邏輯的函數(shù),第3個參數(shù)表示每一批創(chuàng)建的節(jié)點數(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é)點都已經(jīng)被創(chuàng)建好 return clearInterval( t ); } start(); }, 200 ); // 分批執(zhí)行的時間間隔,也可以用參數(shù)的形式傳入 }; };
最后我們進行一些小測試,假設(shè)我們有1000個好友的數(shù)據(jù),我們利用timeChunk函數(shù),每一批只往頁面中創(chuàng)建8個節(jié)點:
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ā)中,因為瀏覽器之間的實現(xiàn)差異,一些嗅探工作總是不可避免。比如我們需要一個在各個瀏覽器中能夠通用的事件綁定函數(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 ); } };
這個函數(shù)的缺點是,當(dāng)它每次被調(diào)用的時候都會執(zhí)行里面的if條件分支,雖然執(zhí)行這些if分支的開銷不算大,但也許有一些方法可以讓程序避免這些重復(fù)的執(zhí)行過程。
第二種方案是這樣,我們把嗅探瀏覽器的操作提前到代碼加載的時候,在代碼加載的時候就立刻進行一次判斷,以便讓addEvent返回一個包裹了正確邏輯的函數(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ù)依然有個缺點,也許我們從頭到尾都沒有使用過addEvent函數(shù),這樣看來,前一次的瀏覽器嗅探就是完全多余的操作,而且這也會稍稍延長頁面ready的時間。
第三種方案即是我們將要討論的惰性載入函數(shù)方案。此時addEvent依然被聲明為一個普通函數(shù),在函數(shù)里依然有一些分支判斷。但是在第一次進入條件分支之后,在函數(shù)內(nèi)部會重寫這個函數(shù),重寫之后的函數(shù)就是我們期望的addEvent函數(shù),在下一次進入addEvent函數(shù)的時候,addEvent函數(shù)里不再存在條件分支語句:
<html> <body> <div id="div1">點我綁定事件</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>
- 零基礎(chǔ)搭建量化投資系統(tǒng):以Python為工具
- Mastering matplotlib
- The React Workshop
- Getting Started with Greenplum for Big Data Analytics
- C#實踐教程(第2版)
- 常用工具軟件立體化教程(微課版)
- Angular應(yīng)用程序開發(fā)指南
- JQuery風(fēng)暴:完美用戶體驗
- Practical Predictive Analytics
- INSTANT Apache ServiceMix How-to
- MATLAB 2020 GUI程序設(shè)計從入門到精通
- Java核心編程
- Spring Web Services 2 Cookbook
- C語言程序設(shè)計實驗指導(dǎo)與習(xí)題精解
- Learning Rust