Skip to content

理解JavaScript柯里化

函数式编程经常使用柯里化,它是一种将接受多个参数的函数转换为一系列接受单个参数的函数的过程。这使得代码更加灵活和可重用。

对函数进行柯里化

在某些情况下,创建一个curry()函数可能会很棘手。为了简单起见,让我们从一个参数数量固定的函数开始。在这种情况下,我们将使用Function.prototype.length来确定函数接受的参数数量。

知道了调用函数所需的参数数量,我们将使用递归来创建一个柯里化函数,一旦提供的参数数量足够,它将调用原始函数。如果不够,它将返回一个新函数,该函数将接受剩余的参数。

const curry = (fn) => {
  const curried = (...args) => (
    args.length >= fn.length
      ? fn(...args)
      : (...rest) => curried(...args, ...rest)
  );
  return curried;
};

const add = (x, y) => x + y;
const curriedAdd = curry(add);
curriedAdd(1)(2); // 3

处理可变参数函数

可变参数函数是指接受可变数量参数的函数。例如,Math.min()是一个可变参数函数,它返回零个或多个数字中的最小值。

[!NOTE]

Math.min.length为2,这弱化地表明它被设计为至少处理两个参数。然而,这与用户定义的可变参数函数不一致。

我们之前的实现方法根本行不通。事实上,对于可变参数函数,Function.prototype.length会返回0。为了解决这个问题,我们应该能够定义我们想要柯里化的函数的arity(参数个数)。这样,柯里化函数就会知道何时停止期望更多的参数并调用原始函数。

除了这个改变,我们还可以期望提前提供一些参数。当我们想要部分应用一个函数时,这是很有用的。我们可以使用Function.prototype.bind()来返回一个期望剩余参数的柯里化函数,而不是使用之前依赖闭包的方法。

将所有东西放在一起,我们得到了一个更健壮和更短的实现,尽管有点不太可读。

const curry = (fn, arity = fn.length, ...args) =>
  arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);

curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2

反柯里化一个函数

那么相反的情况呢?我们如何反柯里化一个函数? 我们仍然需要知道函数的arity,但这次我们将使用Array.prototype.reduce()来调用函数的每个后续柯里化级别。如果参数的数量不足,我们将抛出一个错误。否则,我们将使用Array.prototype.slice()以正确的参数数量调用函数。

const uncurry = (fn, arity = 1) => (...args) => {
  const next = acc => args => args.reduce((x, y) => x(y), acc);
  if (arity> args.length) throw new RangeError('Arguments too few!');
  return next(fn)(args.slice(0, arity));
};

const add = x => y => z => x + y + z;
const uncurriedAdd = uncurry(add, 3);
uncurriedAdd(1, 2, 3); // 6