函数柯里化

老是听大佬们口中谈及函数柯里化,在没有了解之前,我一直认为这是一个十分高深的东西,然而在接触后也深深被他的魅力吸引,那么究竟什么是柯里化?柯里化又有什么好处?作为一个前端工程狮,这是你必须掌握的高级技能。

什么是柯里化(Currying)

柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。也就是将具有多个元的函数转化为具有较少元的函数。

来看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function add(a,b,c){
return a + b + c;
}

function curryingAdd(a){
return function(b){
return function(c){
return a + b + c;
}
}
}

add(1,2,3); //3
curryingAdd(1)(2)(3); //3

Currying的核心思想

柯里化的核心思想就是降低通用性,提高适用性。
举个例子说明

1
2
Ajax("get","url","data");
Ajaxget("url","data");

上述代码中,Ajax是一个通用性的方法,而Ajaxget是一个只针对get请求的方法,从性能上来看,通用性的方法需要对请求的类型进行判断,而通过调用Ajaxget可以减少判断,同时我们可以直接从api上得知请求的类型,对于后期的开发维护也有帮助。

Currying的特点

  • 参数复用:如果是相同的参数,在计算之后不需要再次重新传参计算
  • 提前返回:多次调用多次内部判断,可以直接把第一次判断的结果返回外部接受
  • 延迟执行:避免重复执行程序,等真正需要结果的时候,再执行

参数复用

1
2
3
4
5
6
7
8
9
10
11
12
13
function curryingAdd(a){
return function(b){
return function(c){
return a + b + c;
}
}
}

var add1 = curryingAdd(1);
var add2 = add1(2);

console.log(add2(3));
console.log(add2(4));

还是以刚才的例子做说明,通过复用参数,我们可以实现空间换时间,在调用add2进行计算时,实际上只进行了一次计算。

提前返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var addEvent = function(el, type, fn, capture){
if(window.addEventListener){
el.addEventListener(type, function(){

});
}
else if(window.attachEvent){
el.attachEvent();
}
}

addEvent(div,click,callback,false); //判断一次
addEvent(p,click,callback,false); //判断二次
addEvent(span,click,callback,false); //判断三次

上述代码是兼容浏览器添加事件的一段常用的代码,但在使用中,会多次进行浏览器版本的判断,而浏览器的版本在打开浏览器的时候就已经确定,重复的判断反而会降低使用性能,所以可以利用柯里化思想对上述代码进行下面的改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var addEvent = function(){
if(window.addEventListener){
return function(el, type, fn, capture){
el.addEventListener(type, function(){});
};
}
else if(window.attachEvent){
return function(el, type, fn, capture){
el.attachEvent();
}
}
}

//只判断一次
var addEvent_new = addEvent

addEvent_new(div,click,callback,false);
addEvent_new(p,click,callback,false);
addEvent_new(span,click,callback,false);

延迟执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var curryScore = function(fn){
var _allScore = []; //存放分数
return function(){
if(arguments.length === 0){
return fn.apply(null,_allScore); //对象冒充,形实转换
}
_allScorll = _allScore.concat([].slice.call(arguments));
}
}

var allScore = 0;
var addScore = curryScore(function(){
for(var i = 0; i < arguments.length; i++){
allScore +=arguments[i];
}
});

addScore(2);
addScore(3);
addScore(3);
addScore(2);
addScore(1);
addScore(); //11

上述代码的思想就是先把传入的参数都保存起来,当达到某个延迟条件时,进行输出。

通用封装方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 支持多参数传递
function progressCurrying(fn, args) {

var _this = this
var len = fn.length;
var args = args || [];

return function() {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);

// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args);
}

// 参数收集完毕,则执行fn
return fn.apply(this, _args);
}
}

Currying的性能开销

虽然柯里化提高了函数的适用性,但是它还是会产生一些性能的开销。

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply() 和 fn.call()通常比直接调用fn()稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

但性能的主要瓶颈在操作DOM结点的开销,Currying这部分性能损耗基本可以忽略不计。

经典柯里化面试题

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
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);

// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};

// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}

add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9