在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。(来自百度百科)
首先看一道百度的一个面试题。
实现一个 sumer 函数,功能如下:
sumer(1).getResult(); // 1
sumer(2)(3)(4).getResult(); // 9
sumer(1,2)(3)(4,5).getResult(); // 15
调用 sumer 函数。然后只要调用 getResult 函数就会返回所有参数相加的结果。
sumer 函数很特别,无论参数怎么传入,似乎返回的还是他自身,然后这个函数自身有一个静态属性。
思路
使用闭包来实现
function sumer(...args){
var sum = args.reduce((prev,current) => prev + current);
temp.getResult = function(){
return sum;
}
function temp(...args_2){
sum += args_2.reduce((prev,current) => prev + current);
return sumer(sum);
}
return temp;
}
首先,外层的 sumer 可以接收任意长度的参数,声明了一个 sum 变量,这个变量的值是传入的值之和,然后会返回一个函数(并没有执行) —— temp,sumer 内部定义了 temp 函数,这个函数的参数也是接收任意长的参数,然后计算出参数之和,最后将 sum 传入到 sumer 函数进行执行(执行之后就又会返回 temp 函数,周而复始,直到调用 getResult函数)。
sumer(1,2)
--> sum=3;
--> returntemp;
temp(3)
--> sum=3+3
--> returnsumer(6)
--> sum=6
--> returntemp;
temp(4,5)
--> sum=6+4+5=15
--> returnsumer(15)
--> sum=15
--> returntemp;
temp.getResult();
--> returnsum;
--> 15
如果不太了解闭包的原理,是不太好理解这个函数的。函数内部,sum 变量和 temp 函数形成了一个闭包,sumer 函数运行结束后返回了 temp 函数,而 temp 函数内部却还引用着 sum 变量。而且还有对 sumer 函数的递归。
上面没有定义 temp 就直接 temp.getResult
是因为 JS 代码编译阶段变量会被提升,var 声明的变量、 function
关键字定义的变量都会被提升到外部函数的顶部(这里是 sumer 函数)。
函数柯里化
函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。
有些绕,考虑编写下面这样一个函数:
// add 函数可以接收三个参数,而这三个参数可以像下面一样去调用,
// 而最终返回的结果是一样的
add(1,2,3); // 6
add(1)(2,3); // 6
add(1)(2)(3); // 6
上面的 add 函数就是一个被柯里化的函数,这个函数接收三个参数,但是调用时参数可以传一个或两个或一次直接传三个,而最终返回的值是一样的。如何实现这样的效果呢?
最直接的想法就是接收参数然后在返回一个函数:
function add(...a){
var len = a.length,
res = 3 - len,
sum = a.reduce((prev,current) => prev + current);
return !res ? sum :
function(...b){
len += b.length;
res = 3 - len;
sum += b.reduce((prev,current) => prev + current);
return !res ? sum :
function(c){
return sum + c;
}
}
}
但这是有问题的,假如 add 函数的参数有很 10 个,这时就要返回 10 个,而且还要进行判断。
我们可以定义一个函数,这个函数的参数是一个函数,而返回一个新的函数,这个函数就是被柯里化后的函数。
这个包装函数就可以在第一次就知道被包装的函数有几个参数,在适当的时候返回结果。
function currie(fn){
// 拿到 fn 函数参数的长度
var fnLen = fn.length;
// 返回一个新的函数
return function curred (...args) {
if(args.length >= fnLen){
// 调用这个函数时
// 如果传入的参数与被柯里化的函数参数一样多
// 那就执行被柯里化的函数
return fn.apply(this,args);
}else{
// 否则继续返回一个新的函数
return function(...args_2){
// 每次执行都去判断函数参数够了没有
return curred.apply(this,[...args,...args_2]);
}
}
}
}
这样,currie 函数就成为了一个通用的函数。
function add(a,b,c,d){
return a + b + c + d;
}
var curriedAdd = currie(add);
curriedAdd(1)(2,3)(4); // 10
对于不确定参数的函数,可以这样改进一下:
function currie(fn){
var fnLen = fn.length;
return function curred (...args) {
if(!args.length){
return fn.apply(this,args);
}
return function(...args_2){
if(!args_2.length){
// 如果调用时没有传入参数
// 我们就执行函数
return fn.apply(this,args);
}
return curred.apply(this,[...args,...args_2]);
}
}
}
var add = function(...args){
return args.reduce((p,c) => p + c);
}
var cAdd = currie(add);
cAdd(1)(2,3)(4)(5)(); // 15