什么是柯里化函数?

把接受多个参数的函数变换成先接受部分参数,并且返回一个接收剩余参数的函数,并由此函数返回最终结果。

如果单单是看这一行定义,可能很难理解什么是柯里化。下面我们来举一个例子,假如我们有一个函数,这个函数用来检测一个人是否超过X岁。

表面看起来是一个很简单的需求,函数伸手就来:


const X = 18; function overAge(age){ return age > X }

上面的overAge函数满足了我们的需求。但是这个函数写的非常烂,因为它需要依赖外部的一个值,这样一来,这个函数的重用性就会变的比较差,你可能会说:我把X放到函数里面不就行了吗:


function overAge(age){ const X = 18; return age > X }

没错,这样似乎好了一点,但是程序里写死了一个18,这…如果以后想检测是否大于60岁的那不是还得另写一个函数?再改进一下吧:


function overAge(X, age){ return age > X }

现在我们直接把X作为参数传给函数了,这个时候想检测大于多少岁的都可以!

overAge(18,33);     //true
overAge(18,7);      //false
overAge(60,7);      //false
overAge(60,77);     //true

但是,你是否发现有一些让人不太爽的地方?我如果检测好多个人是否大于18岁,那么每次都要传18这个参数,就显的很奇怪,这是一个相对来说比较固定的值,有没有办法让18只传一次呢?当然有!!!

function overAge(X){
  return function(age){
     return age > X;
  }
}

上面overAge函数接收一个相对于比较固定的值,然后返回一个函数,这个返回函数里面就是做实际的业务操作。没错,这里形成了一个闭包,里面的X在内部函数调用过后并不会被回收。所以我们接下来可以这样使用:

let over18 = overAge(18);
over18(19);
over18(6);

本来我们需要传两个参数,后面我们通过JS闭包的特性把传参进行优化,其实上面的overAge就已经是一个,这样写代码从业务逻辑上也是很容易让人理解的。

那么如果是超过丙个参数要如何处理呢?

Lodash里的curry()

Lodash当中有一个curry()方法。

说到curry ,我默默的看了一眼桌子上的 Curry ……

官方文档给的是这样一个例子:

var abc = function(a, b, c) {
  return [a, b, c];
};

var curried = _.curry(abc);

curried(1)(2)(3);
// => [1, 2, 3]

curried(1, 2)(3);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]

可以看到,函数abc经过curry()之后被柯里化,然后,原本需要传的三个参数,可以通过传给柯里化函数多次的方式进行,而最终得到的结果都是一样的。其实官方这个例子虽然能比较直观的看出curry()能做到的事,但是不太会清楚为什么要这样做。所以还是拿我们上面的例子来看,如果用Lodash的curry()该怎么写那个检测年龄的功能呢?

还是之前的函数,传的是两个参数:

function overAge(X,age){
  return age>X;
}

然后使用curry()来进行柯里化:

let overAgeCurry = _.curry(overAge);
let over18 = overAgeCurry(18);
console.log(over18(6))     //false
console.log(over18(19))    //true

怎么样,这样一写是不是就好理解了?因为我们的这个18是一个相对比较固定的参数,在某种意义上来说,它就相当于一个常量。先进行一次调用,把这个常量封在闭包里,然后再进行后续的传参调用。如果要进行别的年龄判断,那么改变第一次传参即可。

上面的写法好像看起来还是有点不爽,因为多了一个overAgeCurry函数,其实这一层不需要晾出来,会让人有点不好理解,所以我们可以做这样的修改:

let overAge = _.curry(function (X,age){
  return age>X;
});
let over18 = overAge(18);
console.log(over18(6))     //false
console.log(over18(19))    //true

这样,就只有一个表面看起来很好理解的overAge函数。

curry() 的实现方法

其实柯里化主要是运用函数的闭包特性、函数式的编程思想来实现,下面我们自己来实现一下curry()函数:

首先,我们来思考一下,这个函数需要什么参数?返回什么参数?从上面的调用来看,传参和返回参数都是一个函数:

function curry(fn){
  return function(...args){

  }
}

然后,我们需要考虑两种情况:

  • 1.如果柯里化后的函数 实参.length >= 形参.length,那么直接返回fn的执行结果。
  • 2.如果1不满足,那么说明实参.length < 形参.length,那么还需要返回另外一个函数来接收剩余的参数。

首先,我们考虑一下情况1,因为比较简单,直接把传过来的参数直接给fn并且执行并返回就可以了:

function curry(fn){
  return function(...args){
    //情况2
    if(args.length < fn.length){
      //...
      return ;
    }

    //情况1
    return fn(...args);
  }
}

下面我们来考虑一下情况2,这个时候我们还需要继续返回一个函数来进行调用,只不过在调用的时候,要把上次的传参和这次的传参进行一个合并,直到我们把所有参数都传完。所以我们需要来做一个递归调用:

function curry(fn){
  //由于需要递归调用,所以给函数取一个名字。
  return function curryFn(...args){
    //情况2
    if(args.length < fn.length){
      //...
      return function(){
        //因为产生了闭包,所以这里的args是之前的参数。
        //而arguments,则是这次传的参数。
        return curryFn(...args,...arguments);
      };
    }
    //情况1
    return fn(...args);
  }
}

下面我们来测试一下自己封装的这个curry()函数:

//去掉 lodash ,调用自己的 curry()>
let overAge = curry(function (X,age){
  return age>X;
});
let over18 = overAge(18);
console.log(over18(6))     //false
console.log(over18(19))    //true