高阶函数
次访问
高阶函数是指满足下列条件之一的函数:
函数可以作为参数进行传递
函数可以作为返回值进行输出JavaScript
语言中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数当做参数传递,还是让函数的执行结果返回给另外一个函数,这两种情形都有很多应用场景。
函数作为参数传递
把函数当做参数进行传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来就可以分离业务代码中变化和不变的部分。
回调函数
在
ajax
异步请求的应用中,回调函数的使用非常频繁。当我们想在ajax
请求返回之后做些事情,但又并不知道请求返回的确切时间时,最常见的方案是把callback
函数当做参数传入发起的ajax
请求的方法中,待请求完成之后执行callback
函数:1
2
3
4
5
6
7var getUserInfo = function( userId, callback ) {
$.ajax( 'http://bubuzou.com/getUserInfo?' + userId, function( data ) {
if ( typeof callback === 'function') {
callback( data );
}
});
};再来看一个例子,假设有这样一个需求,需要创建
100
个div
元素,同时把他们隐藏起来,那么可以看到下面这种实现:1
2
3
4
5
6
7
8
9var 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
未免有点个性化,成为了一个难以复用的函数,并不是每个人创建了节点之后就希望它们立刻被隐藏。于是我们把这段代码抽离出来,用回调函数的形式传入appendDiv
方法:1
2
3
4
5
6
7
8
9
10
11
12
13var 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';
});Array.prototype.sort
Array.prototype.sort
接受一个函数作为参数,这个函数里面封装了数组元素的排序顺序。我们的目的是对数组进行排序,这是不变的部分;但用什么规则去排序这是可变的部分。1
2
3
4// 从小到大排序
[3, 9, 8, 5].sort(function(a, b) {
return a - b;
});
函数作为返回值输出
判断数据的类型
判断一个数据是否是数组,在以往的实现当中,可以基于鸭子类型的概念来判断,比如这个数据有没有
length
属性,有没有sort
方法等。但更好的方法是用Object.prototype.toString
来计算。根据Object.prototype.toString.call( [1, 2, 3] )
总是返回’[object Array]’
,Object.prototype.toStrng.call( ‘str’ )
也总是返回’[object Array]’
得出,
它总是会返回一个类似结构的字符串。于是用循环语句来批量注册类型判断的函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30var 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( '' ); // true
````
- getSingle
下面是一个单例模式的例子:
``` js
var getSingle = function( fn ) {
var ret;
return function() {
return ret || ( ret = fn.apply( this, arguments ));
};
};
// 来看看这个单例的效果
var getScript = getSingle(function() {
return document.createElement( 'script' );
});
var script1 = getScript();
var script2 = getScript();
console.log( script1 === script2 ); // true
高阶函数实现AOP
AOP
(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来后,再通过“动态植入”的方式掺入业务逻辑模块中。这样做的好处是首先保证了业务逻辑模块的纯洁和高内聚性,其实是可以很方便的复用这些日志统计等功能模块。在 JavaScript
中,我们可以通过Function.prototype
来实现 AOP
:
1 | Function.prototype.before = function( beforeFn) { |
高阶函数的其他应用
currying
currying
的概念最早由俄国数学家Moses Schoofinkel
发明,而后由著名的数理逻辑学家Haskell Curry
将其丰富和发展,currying
由此得名。currying
又称部分求值。这里我们讨论的是函数柯里化(function currying
)。一个柯里化函数首先会接受一些参数,接受了这些参数会后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正求值的时候,之前传入的所有参数都会被一次性用于求值。来看一个例子:
假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录几天花掉了多少钱。1
2
3
4
5
6
7
8var monthCost = 0;
var cost = function( money ) {
monthCost += money;
};
cost( 100 ); 第一天
cost( 200 ); 第二天
cost( 300 ); 第三天
console.log( monthCost ); // 600通过这段代码,我们可以看到每天都花了多少钱,但是如果我们只想知道每个月的消费如何的话,那就没必要计算每天的花费了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var 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接下来编写一个通用的柯里化函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25var 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 );
cost( 100 );
cost( 200 );
cost( 300 );
console.log( cost() ); // 600uncurrying
uncurrying
是反柯里化,大概意思是扩大函数的应用范围,将本来只有特定对象才能使用的方法,扩展到更多的对象。
比如我们常常让类数组对象去借用Array.prototype
的方法:1
2
3
4(function() {
Array.prototype.push.call( arguments, 4 );
console.log( arguments ); // [1, 2, 3, 4]
})(1, 2, 3);uncurrying
第一种实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24Function.prototype.uncurrying = function() {
var self = this;
return function() {
var obj = Array.prototype.shift.call( arguments );
return self.apply( obj, arguments );
};
};
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);
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
});uncurrying
的第二种实现:1
2
3
4
5
6Function.prototype.uncurrying = function() {
var self = this;
return function() {
return Function.prototype.call.apply( self, arguments );
}
};函数节流
在
JavaScript
中,大部分的函数都是由用户主动调动触发的。但是也存在少数情况,这些情况下函数的触发并不是又用户直接控制的。这个时候函数就有可能被频繁地调用,而造成大的性能问题。以下几个场景函数将被频繁调用:给window
绑定了onresize
事件的时候,如果存在DOM
相关的操作,那这个时候是非常耗性能的,严重的时候浏览器可能会卡顿;mousemove
事件,如果给某个元素绑定了拖拽事件,那么该函数也会被频繁的触发;在比如上传一个文件的时候,可能需要频繁的通知进度信息等。函数节流就是为了避免函数被频繁地调用而存在的一种解决方案,从而优化性能。通常是用
setTimeout
来实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var throttle = function( 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 || 500 );
}
};
window.onresize = throttle(function() {
console.log( 1 );
}, 500);分时函数
上面我们介绍了一种解决函数被频繁调用的方法。但是有时候,用户确实有这种需求,比如需要在短时间内把
1000
个qq
好友渲染到列表上,这个时候就可能会很卡。但是如果把1000ms
创建1000
个节点,改成每200ms
创建8
个节点。这个时候就避免这种问题。
分时函数接受3
个参数:第一个是创建节点的时候需要用到的数据,第二个是封装了创建节点的函数,第三个是每一批创建的节点数量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var timeChunk = function( ary, fn, count ) {
var obj,
t,
start;
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 ) { // 如果全部节点都已经创建好
clearInterval( t );
}
start();
}, 200);
};
};分时函数有了,现在我们来测试一下。假设有
1000
个好友,利用timeChunk
函数,每批往页面上渲染8
个:1
2
3
4
5
6
7
8
9
10var ary = [];
for ( var i = 0; 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();惰性加载函数
在
web
开发的过程中,因为浏览器之间的实现差异,一些嗅探工作总是避免不了的。比如我们需要一个能在各个浏览器都能通用的事件绑定函数addEvent
,常见的写法如下:1
2
3
4
5
6
7
8var addEvent = function( elem, type, handler ) {
if ( window.addEventListener ) {
return elem.addEventListener( type, handler, false );
}
if ( window.attachEvent ) {
return elem.attachEvent( 'on' + type, handler );
}
};这种写法的缺点是每次调用函数都必须执行里面的if判断,虽然开销不大,但是有办法能避免这种操作:
1
2
3
4
5
6
7
8
9
10
11
12var 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
函数的时候会重写事件绑定函数,在下次进去的时候就会直接执行事件绑定了。1
2
3
4
5
6
7
8
9
10
11
12
13var addEvent = function( elem, type, handler ) {
if ( window.addEventListener ) {
addEvent = function( elem, type, handler ) {
elem.addEventListener( type, handler, false );
}
}
if ( window.attachEvent ) {
addEvent = function( elem, type, handler ) {
elem.attachEvent( 'on' + type, handler );
}
}
addEvent( elem, type, handler );
};