高阶函数是指至少满足下列条件之一的函数:
把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。
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(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。
通常,在 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
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 事件函数里有一些跟 DOM 节点相关的操作,而跟 DOM 节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。
我们给一个 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
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
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 );
};