js設計模式基礎篇(四)之高階函數

高階函數

高階函數是指至少滿足下列條件之一的函數:

  1. 函數可以作爲參數被傳遞。
  2. 函數可以作爲返回值輸出。

函數作爲參數傳遞 

     把函數當作參數傳遞,這代表我們可以抽離出一部分容易變化的業務邏輯,把這部分業務邏輯放在函數參數中,這樣一來可以分離業務代碼中變化與不變的部分。其中一個重要應用場景就是常見的回調函數。

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 ); 
	 }; 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章