JavaScript函数柯里化

什么是函数柯里化

柯里化(Curring),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。

柯里化特点

  • 接受单一参数,将更多的参数通过回调函数来解决。
  • 返回一个新的函数,用于处理所有的想要传入的参数。
  • 需要利用call/apply与arguments伪数组收集参数。
  • 返回的函数正是用来处理收集起来的参数。

需要理解JavaScript函数的隐式转换

JavaScript是一种弱类型语言,它的隐式转换是非常灵活的。
如下:

1
2
3
4
function fn(){
return 5;
}
console.log(fn+10);

试着运行一下你会发现答案为:

1
2
3
function fn(){
return 5;
}10

接着我们在修改代码为:

1
2
3
4
5
6
7
function fn(){
return 5;
}
fn.toString = function(){
return 10;
}
console.log(fn+10);

你会发现答案为:20,接着我们在进行修改

1
2
3
4
5
6
7
8
9
10
function fn(){
return 5;
}
fn.toString = function(){
return 10;
}
fn.valueOf = function(){
return 20;
}
console.log(fn+10);

答案为30

当我们使用console.log()或者进行运算时,隐式转换就可能会发生。从上面的例子我们可以得到如下结论:

当我们没有重新定义toString()与valueOf时,函数的隐式转换会调用默认的toString方法,他会将函数的定义内容转化为字符串返回。当我们主动定义了toString()/valueOf时,那么隐式转化的返回结果则由我们自己控制了。其中valueOf的优先级会比toString()高。

需要知道如何利用call/apply封装数组的map方法

map:对数组的每一项运行给定的函数,将每次函数调用返回的结果组成新的数组。

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Array.prototype._map = function(fn,ctx){
var list = this,
temp = []; //用来存储返回的新值
console.log(list);
if(typeof fn =='function'){
//遍历数组的每一项
list.forEach(function(item,index){
temp.push(fn.call(ctx,item,index));
});
}else{
console.err('TypeError:'+fn+' must be a function');
}
return temp;
}
var arr = [2,3,4,5]._map(function(item,index){
return item + index;
});
console.log(arr);

理解函数柯里化

考虑实现一个add方法,使结果能够满足如下预期:
add(1)(2) = 3
add(1,2,3)(4) = 10
add(1)(2)(3)(4)(5) = 15

一开始如果只有两个参数,你可能会这样写:

1
2
3
4
5
function add(a){
return function(b){
return a+b;
}
}

如果有三个的话,可以这样写:

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

如果有n个呢,难道要一直嵌套下去,而且2,3种情况也没有覆盖到。所以,要利用函数柯里化,利用闭包的特性,将所有参数,集中到最后的返回函数里进行计算并返回结果。因此,我们在写代码时,要将所有的参数集中起来处理。
具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getSum(rest){
var sum = 0;
rest.forEach((item)=>{
return sum+=item;
});
return sum;
}
function add (...rest){
var _args = rest;
var _adder = function(...rest){
[].push.apply(_args, rest);
return _adder;
}
_adder.toString = function(){
return getSum(_args);
}
return _adder();
}

我们再来考虑函数柯里化的例子

假如有一个计算要求,需要我们将数组里面的每一项用我们自己想要的字符给连接起来。我们该怎么做?

1
2
3
4
5
6
var arr = [1,2,3,4,5];
Array.prototype.merge = function(chars){
return this.join(chars);
}
var str = arr.merge('-');
console.log(str);

在考虑将数组每一位加一位数在连接起来

1
2
3
4
5
6
7
8
var arr = [1,2,3,4];
Array.prototype.merge = function(chars,number){
return this.map((item)=>{
return item+number;
}).join(chars);
}
var str = arr.merge('-',7);
console.log(str);

我们并不知道自己要对数组进行何种处理,所以我们要把对数组的操作封装起来。我们现在只知道需要将他们处理后用字符串连接起来,所以不妨将他们的处理内容保存在函数中,而仅仅固定封装连起来的部分。

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
Array.prototype.merge = function(fn,chars){
return this.map(fn).join(chars);
}
var arr = [1,2,3,4];
var add = function(num){
return function(item){
return num + item;
}
}
var reduce = function(num){
return function(item){
return item - num;
}
}
//每一项加2合并
var res1 = arr.merge(add(2),'-');
//每一项减2合并
var res2 = arr.merge(reduce(2),'-');
//也可以直接传入回调函数
var res3 = arr.merge((function(num){
return function(item){
return item + num;
}
})(2),'-');

柯里化通用式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var currying = function(fn){
var args = [].slice.apply(arguments,[1]);
return function(){
var _args = args.concat([].slice.apply(arguments));
return fn.apply(null,_args);
}
}
var sum = currying(function(){
var list = [].slice.call(arguments);
return list.reduce(function(a,b){
return a + b;
});
},10);

柯里化与bind

1
2
3
4
5
6
7
Object.prototype.bind = function(ctx){
var _args = [].slice.call(arguments,1);
var fn = this;
return function(){
fn.apply(ctx,_args.concat([].slice.call(arguments)));
}
}

参考链接 函数柯里化