函数柯里化

什么是柯里化呢?在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

柯里化为实现多参函数提供一个递归降解的思路,就是将接受多个参数的函数转换成只接收一个参数的函数,并且返回一个新函数,这个新函数会接受剩下的参数。

在 Javascript 中,柯里化是怎样的呢?举个栗子:

function add(num1, num2) {
  return num1 + num2;
}
//假设有一个curry函数可以实现下面的功能
let func = curry(1);
console.log(func(2)); //3

如何实现这个函数呢?

function curry(x) {
  return function(y) {
    return x + y;
  };
}
let func = curry(1);
console.log(func(2)); //3

功能是实现了,但是我们可以看到,这个函数唯一的功能就是进行加法的计算,不能够被复用,所以这并不是真正的 curry 函数。
如果要实现函数复用,就需要将需要执行的函数也一并传入:

function curry(func, ...arg1) {
  console.log(arg1.length);
  return function(...arg2) {
    return func(...arg1, ...arg2);
  };
}
function add(num1, num2) {
  return num1 + num2;
}
let func = curry(add, 1);
console.log(func(2));

这样就算是实现了 curry 函数的基本功能了,但是真正的 curry 函数类似于下面这样:

function curry(func, ...arg1) {
  if (arg1.length >= func.length) {
    return func(...arg1);
  }
  return function(...arg2) {
    return curry(func, ...arg1, ...arg2);
  };
}

上述代码虽然跟真正的 curry 函数有差异,但是核心思想是有的:将传入 curry 的参数(除去第一个参数以外的参数的集合)个数与执行函数(func)的参数个数进行比较,如果大于或者等于,执行传入的函数(func),否则,继续返回一个函数接收参数。

柯里化用法

有一个这样的面试题:

const result = add(1)(2)(3);
console.log(result); //6

可以这样实现:

function add(num1) {
  let sum = 0;
  sum += num1;
  return function(num2) {
    sum += num2;
    return function(num3) {
      sum += num3;
      return num3;
    };
  };
}

这样做自然是没什么问题,但有一个缺点是这个函数不可复用,必须传三个参数给它才会返回结果,如果我想要传四个参数给它呢?

如果用柯里化来做会怎么样呢?

function add(num1) {
  let sum = 0;
  sum += num1;
  return function temp(num2) {
    if (arguments.length === 0) {
      return sum;
    } else {
      sum += num2;
      return temp;
    }
  };
}
add(1)(2)(3)(4)(5)(); //14

这样的话,如果没有传入参数,就会返回 sum 的值,这样就可以无限传参数了,但是我们可以看到这个函数有了两个功能:加法的运算,可以多次调用传参,如果我们还有别的函数需要柯里化,那么我们又要照葫芦画瓢地写一个几乎一摸一样的函数出来,这样会非常的麻烦,所以应该尽量将函数功能单一化:

function curry(func) {
  let args = [];
  return function decide() {
    if (arguments.length === 0) {
      return func.apply(this, args);
    }
    Array.prototype.push.apply(args, [].slice.call(arguments));
    return decide;
  };
}
let add = function() {
  let sum = 0;
  Array.prototype.slice.call(arguments).forEach(function(item) {
    sum += item;
  });
  return sum;
};
const result = curry(add)(1)(2, 3)(4, 5, 6)(); //21

这样就完成了一个通用的柯里化函数

使用场景

那么,函数柯里化在什么时候起作用呢?

增强了函数参数的复用性

function curry(func, ...arg1) {
  if (arg1.length >= func.length) {
    return func(...arg1);
  }
  return function(...arg2) {
    return curry(func, ...arg1, ...arg2);
  };
}
function add(num1, num2) {
  return num1 + num2;
}
let fun = curry(add, 5);
fun(2); //7
fun(3); //8

可以多次调用 fun 方法,最后得出的结果都是在 5 的基础上加上传入的变量值

延迟执行

柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行
假如我想算一下我今年的开销一共花费了多少,我需要的是最终得到的结果,而不是每进行一次运算就返回得到的数字:

function add() {
  let sum = 0;
  return function temp() {
    if (arguments.length === 0) {
      return sum;
    } else {
      if (arguments.length === 1) {
        sum += arguments[0];
      } else {
        Array.prototype.slice.call(arguments).forEach(function(item) {
          sum += item;
        });
      }
    }
  };
}
let result = add();
result(2000);
result(3500);
result(1800);
result(1000, 2000);
result(); //10300

我们可以多次传参,并在最后一次不传参数以获得返回的结果,今年花了 10300,没毛病!

性能

说了这么多。柯里化的函数性能如何呢?
事实证明,它的速度,和内置函数 bind 根本没得比,虽然同是运用了柯里化,但由于 bind 是浏览器实现的,所以速度快也很正常啦,而且 bind 方法不具备多次传递多个参数的功能。

缺陷

除了性能较低之外,从函数柯里化这个名字,我们可以简单理解为函数和柯里化是父子关系,正是有了函数才有了柯里化。柯里化诞生于函数式编程,也服务于函数式编程。另外柯里化的实现实际上也有其他的解决方案,用 bind 或箭头函数提前绑定参数效率要更高一些。