函数柯里化

6 minute read

函数柯里化是一种函数式编程的高阶技术,函数柯里化相当于一个翻译器,将形如f(a,b,c)的函数调用转化为f(a)(b)(c),或者转化回去。柯里化(Currying)不会调用一个函数,而只是转化。

下面的两个🌰同样出现在常见JS代码题

Talk is cheap

🌰一

function add() {
  var _args = [].slice.call(arguments); // line 2
  var adder = function () { // line 3
    var calculate = function() { // line 4
      [].push.apply(_args, [].slice.call(arguments)); // line 5
      return calculate;
    };
    calculate.toString = function () {
      return _args.reduce(function (a, b) {
        return a + b;
      });
    }
    return calculate;
  }
  return adder.apply(null, [].slice.call(arguments)); // line 15 
}
console.log(add(1, 2, 3, 4, 5).toString());  // 15
console.log(add(1, 2, 3, 4)(5).toString());  // 15
console.log(add(1)(2)(3)(4)(5).toString());  // 15

逐行分析

  • 首先在 line 2 位置,_args 顾名思义就是函数运行所需要的参数,它首先会存储 add 第一次执行时的参数例如上面例子中分别为 1,2,3,4,51,2,3,41
  • 然后,可以看到 line 15 行,add 返回的是 adder,并且传入了第一次调用的 arguments,即上述的:1,2,3,4,51,2,3,41
  • 所以看回 line 3 的 adder,adder 的唯一作用就是返回又是一个函数 calculate,因此我们关注 calculate;
  • calculate 首先将当前 arguments push 到 _args 中(注意,这里的 arguments 一定不是上面说到的第一次调用时的 arguments,见后面解释),然后再返回一个 calculate 以供下一次调用访问。
  • toString 方法在函数运行时会自动执行(应该是这样吧),其实也可以换成一个自定义的方法,反正它的作用是计算在最后所有参数全部集中到一个 _args 之后,对参数的集中操作。

解析

console.log(add(1)(2)(3)(4)(5).toString()); 为例,如果我们最后不显示的调用 toString(),我们会发现打印出来的是一个函数 —— calculate,这里的函数就是我们前一个 calculate 返回的 calculate,这里形成了一个闭包,外部定义的 _args 变量会一直保存在 adder 里,这也是能够把后面每一次调用都统一起来的原因。

优化

上面的代码可以更简洁吗?可以

function add() {
  var _args = [];
  var adder = function () {
    [].push.apply(_args, [].slice.call(arguments));
    return adder;
  }
  adder.toString = function () {
    return _args.reduce(function (a, b) {
      return a + b;
    });
  }
  return adder.apply(null, [].slice.call(arguments));
}
console.log(add(1, 2, 3, 4, 5).toString());  // 15
console.log(add(1, 2, 3, 4)(5).toString());  // 15
console.log(add(1)(2)(3)(4)(5).toString());  // 15

前面说过,js 柯里化的精髓就在如何将后续的参数追加到一个数组中,而也说过,闭包存在于 adder 中,所以自然可以直接在 adder 中进行相应的操作。

Show me your code

🌰二

function curry (fn, args) {
  var length = fn.length;
  args = args || [];
  return function () {
    var _args = args.slice (0), i;
    for (i = 0; i < arguments.length; i++) {
      _args.push (arguments[i]);
    }
    if (_args.length < length) {
      return curry.call (this, fn, _args);
    } else {
      return fn.apply (this, _args);
    }
  };
}
var add = curry (function (a, b, c) {
  console.log (a + b + c);
});
add (1, 2, 3); // 6
add (1) (2, 3); // 6
add (1) (2) (3); // 6

分析

具体的原理不再赘述,柯里化的思想就是参数集合起来。

  • 这里同样有一个闭包,在这个闭包中同样保持着一个变量 lengthargs,这里的 length 就是函数的参数个数,上面的例子中,参数个数就是3;
  • 这里通过对 _args 中追加每一次调用的参数,并将其与函数长度 —— length 做比较,很好理解,如果参数的个数已经大于等于函数所需的参数个数,那么自然直接返回函数执行即可。

🌰对比

例子一中的代码可以不用指定参数个数,因为我们需要在最后都显示的执行一次 toString 方法。而例子二需要指定是因为我们创建的 curry 函数里,有判断相关参数个数的代码。

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.