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.onresize=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 ); 
	 }; 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值