高階函數
高階函數是指至少滿足下列條件之一的函數:
- 函數可以作爲參數被傳遞。
- 函數可以作爲返回值輸出。
函數作爲參數傳遞
把函數當作參數傳遞,這代表我們可以抽離出一部分容易變化的業務邏輯,把這部分業務邏輯放在函數參數中,這樣一來可以分離業務代碼中變化與不變的部分。其中一個重要應用場景就是常見的回調函數。
1. 回調函數
在 ajax 異步請求的應用中,回調函數的使用非常頻繁
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 );
});
回調函數的應用不僅只在異步請求中,當一個函數不適合執行一些請求時,我們也可以把這些請求封裝成一個函數,並把它作爲參數傳遞給另外一個函數,“委託”給另外一個函數來執行。
比如,我們想在頁面中創建 100 個 div 節點,然後把這些 div 節點都設置爲隱藏。下面是一種編寫代碼的方式:
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 未免有點個性化,成爲了一個難以複用的函數,於是我們把 div.style.display = 'none'這行代碼抽出來,用回調函數的形式傳入 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 callbaback === 'function' ){
callback( div );
}
}
};
appendDiv(function( node ){
node.style.display = 'none';
});
可以看到,隱藏節點的請求實際上是由客戶發起的,但是客戶並不知道節點什麼時候會創建好,於是把隱藏節點的邏輯放在回調函數中,“委託”給 appendDiv 方法。appendDiv 方法當然知道節點什麼時候創建好,所以在節點創建好的時候,appendDiv 會執行之前客戶傳入的回調函數。
2. Array.prototype.sort
Array.prototype.sort 接受一個函數當作參數,這個函數裏面封裝了數組元素的排序規則。從Array.prototype.sort 的使用可以看到,我們的目的是對數組進行排序,這是不變的部分;而使用什麼規則去排序,則是可變的部分。把可變的部分封裝在函數參數裏,動態傳入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 ]
函數作爲返回值輸出
相比把函數當作參數傳遞,函數當作返回值輸出的應用場景也許更多,也更能體現函數式編程的巧妙。讓函數繼續返回一個可執行的函數,意味着運算過程是可延續的。
1. 判斷數據的類型
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
我們還可以用循環語句,來批量註冊這些 isType 函數:
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
下面是一個單例模式的例子,這裏暫且只瞭解其代碼實現 以後我們將進行更深入的講解。
var getSingle = function ( fn ) {
var ret;
return function () {
return ret || ( ret = fn.apply( this, arguments ) );
};
};
這個高階函數的例子,既把函數當作參數傳遞,又讓函數執行後返回了另外一個函數。我們可以看看 getSingle 函數的效果
var getScript = getSingle(function(){
return document.createElement( 'script' );
});
var script1 = getScript();
var script2 = getScript();
alert ( script1 === script2 ); // 輸出:true
高階函數實現AOP
AOP(面向切面編程)的主要作用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能通常包括日誌統計、安全控制、異常處理等。把這些功能抽離出來之後,再通過“動態織入”的方式摻入業務邏輯模塊中。這樣做的好處首先是可以保持業務邏輯模塊的純淨和高內聚性,其次是可以很方便地複用日誌統計等功能模塊。
通常,在 JavaScript 中實現 AOP,都是指把一個函數“動態織入”到另外一個函數之中,具體的實現技術有很多,本節我們通過擴展 Function.prototype來做到這一點。代碼如下:
// 函數是一個對象
Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函數的引用
return function(){ // 返回包含了原函數和新函數的"代理"函數
beforefn.apply( this, arguments ); // 執行新函數,修正 this
return __self.apply( this, arguments ); // 執行原函數
}
};
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 );
};
// 鏈式調用 before()和after()都會返回一個代理函數
func = func.before(function(){
console.log( 1 );
}).after(function(){
console.log( 3 );
});
func();
最終結果 輸出1,2,3
這種使用 AOP 的方式來給函數添加職責,也是 JavaScript 語言中一種非常特別和巧妙的裝飾者模式實現。這種裝飾者模式在實際開發中非常有用,我們會在後面詳細學習。
高階函數的其他應用
1. currying
currying 又稱部分求值。一個 currying 的函數首先會接受一些參數,接受了這些參數之後,該函數並不會立即求值,而是繼續返回另外一個函數,剛纔傳入的參數在函數形成的閉包中被保存起來。待到函數被真正需要求值的時候,之前傳入的所有參數都會被一次性用於求值。
假設我們要編寫一個計算每月開銷的函數。在每天結束之前,我們都要記錄今天花掉了多少錢,然後在月底計算一次。
// fn 求和函數
var curring=function(fn){
var arg=[];
return function(){
// 參數爲空求和
if(arguments.length===0){
return fn.apply(this,arg);
// 參數不爲空保存參數
} else{
[].push.apply(arg,arguments);
}
}
}
// 求和函數
var sum=(function(){
var m=0;
return function(){
for(var i=0,len=arguments.length;i<len;i++){
m+=arguments[i];
}
return m;
}
})()
//
var cost=curring(sum);
cost(100);
cost(200);
cost(300);
console.log(cost()); // 600
2.uncurrying
在我們的預期中,Array.prototype 上的方法原本只能用來操作 array 對象。但用 call 和 apply可以把"任意"對象當作 this 傳入某個方法,這樣一來,方法中用到 this 的地方就不再侷限於原來規定的對象,而是加以泛化並得到更廣的適用性。
把“任意”兩字加了雙引號,是因爲可以借用 Array.prototype.push 方法的對象還要滿足以下兩個條件:
1.對象本身要可以存取屬性;
2..對象的 length 屬性可讀寫; (函數的length屬性是隻讀屬性)
那麼有沒有辦法把泛化 this 的過程提取出來呢?uncurrying 就是用來解決這個問題的。
Function.prototype.uncurrying = function () {
var self =this;
return function() {
var obj = Array.prototype.shift.call(arguments); // 截取第一個參數作爲this
console.log("內部arguments",arguments); // [4]
return self.apply(obj,arguments);
}
}
var push =Array.prototype.push.uncurrying();
(function(){
push(arguments,4);
console.log("外部arguments",arguments); // 輸出: [1,2,3,4]
})(1,2,3);
我們還可以一次性地把 Array.prototype 上的方法“複製”到 array 對象上,同樣這些方法可操作的對象也不僅僅只是 array 對象:
for(var i=0,fn;fn=['push','shift','forEach'][i++];){
Array[fn]=Array.prototype[fn].uncurrying();
}
var obj={
'length':3,
'0':1,
'1':2,
'2':3
}
Array.push(obj,4);
console.log("obj",obj); // {0: 1, 1: 2, 2: 3, 3: 4, length: 4}
console.log("obj.length",obj.length); // 4
Array.shift(obj);
console.log("obj",obj); // {0: 2, 1: 3, 2: 4, length: 3}
甚至 Function.prototype.call 和 Function.prototype.apply 本身也可以被 uncurrying,不過這沒有實用價值,只是使得對函數的調用看起來更像 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(){
console.log( this.name ); // 輸出:"sven"
console.log( arguments ); // 輸出: [1, 2, 3]
};
apply( fn, { name: 'sven' }, [ 1, 2, 3 ] );
在來分析調用 Array.prototype.push.uncurrying()這句代碼時發生了什麼事情:
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 );
// 相當於 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}
除了剛剛提供的代碼實現,下面的代碼是 uncurrying 的另外一種實現方式:
Function.prototype.uncurrying = function(){
var self = this;
return function(){
return Function.prototype.call.apply( self, arguments );
}
};
3. 函數節流
在一些場景下,函數有可能被非常頻繁地調用,而造成大的性能問題。下面將列舉一些這樣的場景。
(1) 函數被頻繁調用的場景
- window.onresize 事件:
如果在 window.onresize 事件函數裏有一些跟 DOM 節點相關的操作,而跟 DOM 節點相關的操作往往是非常消耗性能的,這時候瀏覽器可能就會吃不消而造成卡頓現象。
- mousemove 事件:
我們給一個 div 節點綁定了拖曳事件(主要是 mousemove),當div 節點被拖動的時候,也會頻繁地觸發該拖曳事件函數
(2) 函數節流的原理
比如我們在 window.onresize 事件中要打印當前的瀏覽器窗口大小,在我們通過拖曳來改變窗口大小的時候,打印窗口大小的工作 1 秒鐘進行了 10 次。而我們實際上只需要 2 次或者 3 次。這就需要我們按時間段來忽略掉一些事件請求,比如確保在 500ms 內只打印一次。很顯然,我們可以藉助 setTimeout 來完成這件事情。
var throttle = function(fn,interval){ // fn要延遲執行的函數,interval:延遲執行的時間
var __self = fn, // 保存需要被延遲執行的函數引用
timer, // 定時器
firstTime =true; // 是否是第一次調用
return function () {
var args =arguments,
__me = this;
if(firstTime){ // 如果是第一次調用,不需要延遲執行
__self.apply(__me,args);
return firstTime = false;
}
if(timer){ // 如果定時器還在,說明前一次延遲執行還沒有完成
return false;
}
timer = setTimeout(function(){ // 延遲一段時間執行
clearTimeout(timer);
timer = null;
__self.apply(__me,args);
},interval || 2000);
}
}
window.οnresize=throttle(function(){
console.log(1);
},2000)
4.分時函數
某些函數確實是用戶主動調用的,但因爲一些客觀的原因,這些函數會嚴重地影響頁面性能。
一個例子是創建 WebQQ 的 QQ 好友列表。列表中通常會有成百上千個好友,如果一個好友用一個節點來表示,當我們在頁面中渲染這個列表的時候,可能要一次性往頁面中創建成百上千個節點。
在短時間內往頁面中大量添加 DOM 節點顯然也會讓瀏覽器吃不消,我們看到的結果往往就是瀏覽器的卡頓甚至假死。
var ary=[];
for(var i=1;i<=1000;i++)}
ary.push(i); // 假設ary裝載了1000個好友的數據
}
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 函數讓創建節點的工作分批進行,比如把 1 秒鐘創建 1000 個節點,改爲每隔 200 毫秒創建 8 個節點
timeChunk 函數接受 3 個參數,第 1 個參數是創建節點時需要用到的數據,第2個參數是封裝了創建節點邏輯的函數,第 3 個參數表示每一批創建的節點數量。
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){ // 如果全部節點都已經被創建好
return clearInterval(t);
}
start();
},200) //分批執行的時間間隔,也可以用參數的形式傳入
}
}
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.惰性加載函數
在 Web開發中,因爲瀏覽器之間的實現差異,一些嗅探工作總是不可避免。比如我們需要一個在各個瀏覽器中能夠通用的事件綁定函數 addEvent,常見的寫法如下:
var addEvent = function(elm,type,handler){
if(window.addEventListener){
return elm.addEventListener(type,handler,false);
}
if(window.attachEvent){
return elm.attachEvent('on'+type,handler);
}
}
這個函數的缺點是,當它每次被調用的時候都會執行裏面的 if 條件分支,雖然執行這些 if分支的開銷不算大,但也許有一些方法可以讓程序避免這些重複的執行過程。
第二種方案是這樣,我們把嗅探瀏覽器的操作提前到代碼加載的時候,在代碼加刻進行一次判斷以便讓 addEvent 返回一個包裹了正確邏輯的函數。代碼如下:
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 函數依然有個缺點,也許我們從頭到尾都沒有使用過 addEvent 函數,這樣看來,前一次的瀏覽器嗅探就是完全多餘的操作,而且這也會稍稍延長頁面 ready 的時間。
第三種方案即是我們將要討論的惰性載入函數方案。此時 addEvent 依然被聲明爲一個普通函數,在函數裏依然有一些分支判斷。但是在第一次進入條件分支之後,在函數內部會重寫這個函數,重寫之後的函數就是我們期望的 addEvent 函數,在下一次進入 addEvent 函數的時候,addEvent函數裏不再存在條件分支語句:
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 );
};