Skip to content

函数柯里化 (Currying)

1. 什么是柯里化?

柯里化(Currying)是函数式编程这里的一个重要概念。它是指将一个接受多个参数的函数,将其转换为一系列接受单一参数的函数的过程。

简单来说,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

$$ f(a, b, c) \rightarrow f(a)(b)(c) $$

2. 为什么要使用柯里化?

柯里化主要有以下几个核心优势:

2.1 参数复用

当在多次调用中,某些参数是相同的,柯里化可以帮助我们固定这些参数,生成一个新的函数,避免重复传递相同的参数。

2.2 延迟执行

柯里化的函数不会立即执行,而是不断地收集参数,直到参数数量满足要求时才会执行真正的逻辑。这也可以看作是一种"预加载"函数。

2.3 函数组合的基础

在函数组合(Composition)中,我们需要单参数的函数(Unary Function)。柯里化可以将多元函数转化为一元函数,使其能够顺畅地融入函数管道(Pipe)中。

3. 代码实现

3.1 简单实现 (手动嵌套)

最基础的柯里化就是利用闭包返回函数。

javascript
// 普通函数
function add(x, y) {
    return x + y;
}

// 柯里化后的函数
function curriedAdd(x) {
    return function(y) {
        return x + y;
    }
}

// 使用箭头函数简化
const addES6 = x => y => x + y;

add(1, 2);          // 3
curriedAdd(1)(2);   // 3

3.2 通用实现 (自动柯里化)

我们可以编写一个通用的 curry 函数,将任何普通函数转换为柯里化函数。

javascript
function curry(fn) {
  // 1. 获取原函数需要的参数个数
  const arity = fn.length;

  return function curried(...args) {
    // 2. 如果当前收集的参数个数 >= 原函数需要的参数个数
    if (args.length >= arity) {
      // 执行原函数
      return fn.apply(this, args);
    } else {
      // 3. 否则返回一个新函数,继续收集剩余的参数
      return function (...nextArgs) {
        // 将之前的参数(args)和新的参数(nextArgs)合并,递归调用 curried
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}

使用示例:

javascript
function sum(a, b, c) {
    return a + b + c;
}

const curriedSum = curry(sum);

curriedSum(1, 2, 3); // 6
curriedSum(1)(2, 3); // 6
curriedSum(1)(2)(3); // 6

4. 实际应用场景

4.1 正则校验工具库

假设我们有一个通用的正则校验函数 checkByRegExp,我们可以通过柯里化衍生出各种特定的校验函数。

javascript
const checkByRegExp = (regExp, string) => regExp.test(string);

const curriedCheck = curry(checkByRegExp);

// 生成特定的校验函数
const isMobile = curriedCheck(/^1\d{10}$/);
const isEmail = curriedCheck(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

// 使用
isMobile('13800138000'); // true
isEmail('test@example.com'); // true

4.2 DOM 事件监听兼容处理

在处理浏览器兼容性时,我们可以利用柯里化的延迟执行特性,只在第一次调用时进行环境判断,后续调用直接使用绑定好的函数。

javascript
const addEvent = (function() {
    if (window.addEventListener) {
        return function(element, type, listener, useCapture) {
            element.addEventListener(type, function(e) {
                listener.call(element, e);
            }, useCapture);
        };
    } else if (window.attachEvent) {
        return function(element, type, listener) {
            element.attachEvent("on" + type, function(e) {
                listener.call(element, e);
            });
        };
    }
})();
// 这里虽然是一个立即执行函数返回的闭包,也体现了类似柯里化的思想:预先固定某些环境参数或逻辑。

4.3 配合 Map/Filter/Reduce

在处理数组时,柯里化函数非常有用,尤其是当使用 Point-free 风格编程时。

javascript
const add = x => y => x + y;
const multiply = x => y => x * y;

const arr = [1, 2, 3];

// 传统的写法
const result1 = arr.map(item => item * 2).map(item => item + 1);

// 使用柯里化函数
// 注意:map 需要一个函数作为参数,multiply(2) 返回的正是这样一个函数
const result2 = arr.map(multiply(2)).map(add(1));

console.log(result2); // [3, 5, 7]

5. 常见面试题:实现 add(1)(2)(3)

题目:实现一个 add 函数,满足 add(1)(2)(3) 返回 6,并且能够处理任意长度的调用。

思路:这通常涉及到重写函数的 toStringvalueOf 方法,以便在最后求值时返回结果。

javascript
function addInfinite(...args) {
    // 内部函数,负责收集参数
    const _adder = function(...nextArgs) {
        return addInfinite(...args, ...nextArgs);
    };

    // 重写 valueOf 或 toString,用于在隐式转换时计算结果
    _adder.toString = function() {
        return args.reduce((a, b) => a + b, 0);
    };

    return _adder;
}

// 注意:结果是一个函数,但在 alert 或 console.log (某些环境) 或者是参与计算时会调用 toString
// console.log(+addInfinite(1)(2)(3)); // 6 (通过 + 触发隐式转换)

6. 总结

  • 柯里化是将多参数函数转换为单参数函数序列的过程。
  • 它有助于参数复用,提高代码的模块化程度。
  • 它是函数组合的重要基础。
  • 实现的关键在于闭包参数收集

返回函数式编程目录

MIT Licensed | Keep Learning.