深入理解 JavaScript 柯里化(Currying):从加法到日志函数的实战解析

48 阅读4分钟

在函数式编程中,柯里化(Currying) 是一个既优雅又实用的概念。它能让我们将一个多参数函数,转化为一系列只接收单个参数的函数。本文将通过手写代码、逐步拆解,带你彻底掌握 JavaScript 中的柯里化机制——从最简单的 add(1)(2) 到实际项目中的日志封装。


一、什么是柯里化?

柯里化(Currying) :把一个接受多个参数的函数,转换成一系列只接受一个参数的函数,并在所有参数收集完毕后执行原函数。

🌰 最直观的例子:

普通函数:

js
编辑
function add(a, b) {
  return a + b;
}
console.log(add(1, 2)); // 3

柯里化后:


function curriedAdd(a) {
  return function(b) {
    return a + b;
  };
}
// 或用箭头函数更简洁
const curriedAdd = (a) => (b) => a + b;

console.log(curriedAdd(1)(2)); // 3
  • 先传 1 → 返回一个新函数 (b) => 1 + b
  • 再传 2 → 得到结果 3

这就是柯里化的本质:分步传参,延迟执行


二、柯里化的三大核心机制

1. 闭包(Closure)

每一层函数都能访问外层函数的变量(如 a),这些变量不会被销毁,形成“记忆”。

2. 递归(Recursion)

通过不断返回新函数,实现参数的逐步收集。

3. 退出条件:参数数量到位

如何知道“参数够了”?靠 fn.length —— 函数声明时的形参个数。


function add(a, b, c, d) {
  return a + b + c + d;
}
console.log(add.length); // 4

三、手写通用柯里化函数

我们可以写一个工具函数,自动把任意函数转为柯里化形式:


function curry(fn) {
  return function curried(...args) {
    // 退出条件:当前参数数量 >= 原函数所需参数数量
    if (args.length >= fn.length) {
      return fn(...args); // 执行原函数
    }
    // 否则:返回新函数,继续收集参数(闭包 + 递归)
    return (...rest) => curried(...args, ...rest);
  };
}

🔍 逐行解析:

  • fn:原始函数(如 add
  • curried(...args):当前已收集的参数
  • if (args.length >= fn.length):判断是否收齐
  • (...rest) => curried(...args, ...rest):合并新旧参数,递归调用

✅ 使用示例:


const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3));     // 6
console.log(curriedAdd(1, 2)(3));     // 6(支持一次传多个)
console.log(curriedAdd(1)(2, 3));     // 6
console.log(curriedAdd(1, 2, 3));     // 6

💡 我们的实现兼容“一次传完”或“分多次传”,更灵活!


四、柯里化的实际应用场景

场景 1:创建专用函数(参数复用)


const multiply = (a, b) => a * b;
const curriedMultiply = curry(multiply);

const double = curriedMultiply(2); // 固定第一个参数
const triple = curriedMultiply(3);

console.log(double(5));  // 10
console.log(triple(4));  // 12

避免重复写 multiply(2, x),提升代码复用性。


场景 2:函数语义化(日志系统)

这是前端开发中非常常见的模式:


// 柯里化日志函数
const log = (type) => (message) => {
  console.log(`[${type}]: ${message}`);
};

// 固定日志类型,生成专用函数
const errorLog = log("error");
const infoLog = log("info");
const warnLog = log("warn");

// 使用
errorLog("接口异常");        // [error]: 接口异常
infoLog("页面加载完成");     // [info]: 页面加载完成
warnLog("即将弃用该 API");   // [warn]: 即将弃用该 API

✅ 优势:

  • 代码更清晰
  • 避免每次写 log("error", "...")
  • 易于维护和扩展

场景 3:React 事件处理(带参数)


// 柯里化风格
const deleteItem = (id) => () => {
  // 执行删除逻辑
  api.delete(`/items/${id}`);
};

<button onClick={deleteItem(item.id)}>删除</button>

对比普通写法:


// 需要箭头函数包裹
<button onClick={() => deleteItem(item.id)}>删除</button>

柯里化让 JSX 更简洁、性能更好(避免内联回调)。


五、柯里化 vs 偏函数(Partial Application)

特性柯里化(Currying)偏函数(Partial)
传参方式每次只能传 1 个可以传 多个
返回值总是返回函数,直到参数收齐一次固定部分参数,返回新函数
示例f(a)(b)(c)f(a, b)(c) 或 f(a)(b, c)

📌 在实际使用中,界限常模糊。很多库(如 Lodash)的 _.curry 也支持多参调用。


六、总结:为什么学柯里化?

价值说明
✅ 参数复用创建配置好的专用函数
✅ 代码抽象提升函数组合能力
✅ 语义清晰如 errorLog("...") 比 log("error", "...") 更直观
✅ 函数式编程基础理解高阶函数、组合、管道等概念的前提

⚠️ 注意:不是所有场景都需要柯里化
如果只是临时计算 3 + 5,直接 add(3, 5) 更简单;
但如果你要创建 10 个“加 10”的函数,那 curriedAdd(10) 就非常香!


七、动手试试!

现在,你可以自己实现:


// 1. 柯里化一个四则运算函数
const calc = (op, a, b) => {
  switch(op) {
    case '+': return a + b;
    case '*': return a * b;
  }
};

// 2. 用 curry 包装它
const curriedCalc = curry(calc);

// 3. 创建专用计算器
const adder = curriedCalc('+');
console.log(adder(5)(3)); // 8

结语

柯里化不是炫技,而是一种思维方式

“把复杂问题拆解为一系列简单步骤,并在合适的时机组合它们。”

掌握它,你不仅能写出更优雅的 JavaScript,还能更好地理解现代框架(如 React、Redux)和函数式库(如 Ramda、Lodash/fp)的设计哲学。