最近学习了一下高阶函数,其中印象比较深刻的有函数消抖,函数节流以及函数柯里化(curry),在学习中也是从一头雾水到逐渐明了,所以写一篇文章对柯里化进行总结。

什么是函数柯里化

javascript忍者中说:在一个函数中首先填充几个参数(然后再返回一个新函数)的技术称为柯里化(Currying)。听起来跟bind的作用是一样的,其实bind也可以采用这种思想来实现(至于bind原本是怎么实现的,我不清楚,控制台输出Function.prototype.bind,输出是[native code],看不到,不清楚内部原理)。
在很多文章中写到:柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果。
举个不是很恰当的例子,有一个厨师,要做饭,但是餐馆的小儿没有把菜买齐,这样,小儿买一份原料,放在厨师厨房,再买一份,放在厨师厨房,等买齐了,叫厨师过来,好了,原料齐了,可以做饭了。这个时候,厨师利用原料,把饭做好。厨师就像一个函数,他有自己的功能(做饭),但是参数(原料)不齐,每次执行这个函数,在参数不齐的情况下,只能返回一个新的函数,这个新的函数已经内置了之前的参数,当参数齐了之后完成他本身的功能。

问题

要实现一个这样的加法函数,使得:

add(1,2,3)(1)(2)(3)(4,5,6)(7,8)() === 42

首先,可以看到,这个函数,只有当参数为空的时候,才执行之前所有数值的加法,这样的嵌套可以无限进行,当有参数的时候,add(1,2,3),这个时候的返回值应该是一个函数,这个函数存储了1,2,3但是没有执行加法(执行了也行,此处假设就不执行,只是起到保存参数的作用),这样,继续执行add(1,2,3)(2)()就能输出1+2+3+2=8
要实现这样的一个函数,首先,返回值在参数不为空的时候必定返回一个函数,该函数还保存了之前的参数,这就需要用到闭包。
最终的实现如下:

// add 函数柯里化
function add(){
    //建立args,利用闭包特性,不断保存arguments
    var args = [].slice.call(arguments);
       //方法一,新建_add函数实现柯里化
    var _add = function(){
        if(arguments.length === 0){
            //参数为空,对args执行加法
            return args.reduce(function(a,b){return a+b});
        }else {
            //否则,保存参数到args,返回一个函数
            [].push.apply(args,arguments);
            return _add;
        }
    }
    //返回_add函数
    return _add;

    // //方法二,使用arguments.callee实现柯里化
    // return function () {
  //       if (arguments.length === 0) {
  //           return args.reduce(function(a,b){return a+b});
  //       }
  //       Array.prototype.push.apply(args, arguments);
  //       return arguments.callee;
  //   }
}
console.log(add(1,2,3)(1)(2)(3)(4,5,6)(7,8)());//42

实现的原理主要是:

  1. 闭包保存args变量,存储之前的参数
  2. 新建一个_add函数,参数的长度为0,就执行加法,否则,存储参数到args,然后返回函数自身(可以选择匿名函数,返回arguments.callee即可,意思相同,见代码中注释掉的部分,但是在严格模式下不能使用,所以还是使用方法一比较稳妥)。

通用的函数来对普通函数进行柯里化

可以看出来,柯里化其实是有特点的,需要一个闭包保存参数,一个函数来进行递归,这种模式是可以通过一个包装函数,对一些基本的函数进行包装之后的函数具有curry的特性。实现如下:

//  通用的函数柯里化构造方法
function curry(func){
    //新建args保存参数,注意,第一个参数应该是要柯里化的函数,所以args里面去掉第一个
    var args = [].slice.call(arguments,1);
    //新建_func函数作为返回值
    var _func =  function(){
        //参数长度为0,执行func函数,完成该函数的功能
        if(arguments.length === 0){
            return func.apply(this,args);
        }else {
            //否则,存储参数到闭包中,返回本函数
            [].push.apply(args,arguments);
            return _func;
        }
    }
    return _func;
}

function add(){
    return [].reduce.call(arguments,function(a,b){return a+b});
}
console.log(curry(add,1,2,3)(1)(2)(3,4,5,5)(5,6,6,7,8,8)(1)(1)(1)());//69

上述代码定义了一个add函数,完成参数相加的功能,通过curry(add),使该函数能够进行柯里化,实现延迟执行。也可以采用arguments.callee来实现,原理相同,只是严格模式下不能使用而已。
事实上,我在看函数节流以及函数消抖的大神写法的时候,其实应该把函数执行的上下文也保存下来,才能更加完善,有待后续更正。

柯里化的作用

看了很多文章,比较常见的功能有以下几种:

  1. 事件绑定的时候检测应该用dom几的方式,柯里化的话,只需要检测一次,返回一个新的函数来进行事件绑定(我觉得所有需要判断的地方,如果判断条件在一次访问中不改变的话,都可以写成柯里化的形式,从而达到只进行一次判断的效果,感觉很有用)
  2. 利用柯里化的思想,可以自己写一个bind函数
  3. 延迟执行某个函数,(在一些文章中看到回调函数可以借助这个,还不是很理解,有待之后补充

小结

这篇文章主要是写了一点自己学习函数柯里化之后的总结,总体来说,学的时候很痛苦,学会了很简单。可能知识就是这么神奇,会者不难。

参考文章

  1. 从一道面试题谈谈函数柯里化(Currying)
  2. 简单理解JavaScript中的柯里化和反柯里化
  3. 浅析 JavaScript 中的 函数 currying 柯里化