函数柯里化

在 JavaScript 中,函数柯里化是函数式编程的重要思想,也是高阶函数中一个重要的应用,其含义是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。

例如有一个简单的加法函数,他能够将自身的三个参数加起来并返回计算结果。

1
2
3
function add(a, b, c) {
return a + b + c;
}

那么add函数的柯里化函数_add则可以如下:

1
2
3
4
5
6
7
function _add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}

因此下面的运算方式是等价的。

1
2
add(1, 2, 3);
_add(1)(2)(3);

但是这样的函数扩展性差,只要参数个数超过3个就无法输出预期结果。
我们考虑使用链式调用的写法来解决这个问题,只要还有参数就一直返回函数自身:

1
2
3
4
5
6
7
function add(x) {
var sum = x;
return function tmp(y) {
sum = sum + y;
return tmp;
};
}

但是这样输出的结果并不是一个数字,而是一个函数对象的字符串表示。我们知道,当我们直接将函数参与其他的计算时,函数会默认调用toString方法,直接将函数体转换为字符串参与计算。因此我们可以重写toString方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(x) {
var sum = x;
var tmp = function (y) {
sum = sum + y;
return tmp;
};
tmp.toString = function () {
return sum;
};
return tmp;
}

add(1)(2)(3) // 6

这样就能实现任意参数个数的加法了。但是如果想实现add(1,2)(3)这种形式去随意拆分参数呢?那我们可以用到ES6的扩展运算符来获取参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function add(...args) {
// 在内部声明一个函数,利用闭包的特性保存args并收集所有的参数值
var adder = function () {
var _adder = function(..._args) {
args.push(..._args);
return _adder;
};

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

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

我们接下来封装一个通用的柯里化转换函数,可以将任意已知参数个数的函数转换成柯里化。

1
2
3
4
5
6
7
8
9
10
11
12
13
function currying(func, args = []) {
let arity = func.length;

return function (..._args) {
_args.unshift(...args);
// 现有参数少于函数原来参数长度,就递归调用
if(_args.length < arity) {
return currying.call(null, func, _args);
}
// 已到达最后的参数,直接执行函数
return func(..._args);
}
}

函数 currying 算是比较高级的转换柯里化的通用式,可以随意拆分参数,假设一个被转换的函数有多个形参,我们可以在任意环节传入任意个数的参数进行拆分,举一个例子,假如 5 个参数,第一次可以传入 2 个,第二次可以传入 1 个, 第三次可以传入剩下的,也有其他的多种传参和拆分方案,因为在 currying 内部收集参数的同时按照被转换函数的形参顺序进行了更正。