柯里化(Curry):让函数更灵活、更聪明的编程技巧

30 阅读4分钟

🌟柯里化(Curry):让函数更灵活、更聪明的编程技巧

一句话记住:柯里化 = 把一个需要多个参数的函数,变成可以“分步传参”的函数。

在 JavaScript 函数式编程中,柯里化(Currying) 是一个既优雅又实用的核心概念。它不仅能提升代码复用性,还能让逻辑更清晰、组合更自然。本文将结合三个小例子带你从“是什么”到“怎么写”再到“为什么用”,彻底掌握柯里化。

一、什么是柯里化?

假设你想写一个加法函数,把两个数加起来:

// 普通写法
function add(a, b) {
  return a + b;
}

console.log(add(2, 3)); // 输出:5

这很直接,对吧?但如果我们不能一次拿到两个数呢?

比如:

  • 先知道第一个数是 2
  • 过一会儿才知道第二个数是 3

这时候,普通函数就不好用了。于是,我们可以把它“改造”一下:

function add(a) {
  return function(b) {
    return a + b;
  };
}

// 使用方式变了!
const add2 = add(2);      // 先记住“加2”
console.log(add2(3));     // 再加3 → 输出:5

// 也可以连着写
console.log(add(2)(3));   // 同样输出:5

🔍 这就是最基础的“柯里化”!

  • 原本:add(2, 3)
  • 柯里化后:add(2)(3)

💡 核心思想
先接收一部分参数(比如 a),返回一个新函数;这个新函数再接收剩下的参数(比如 b),最后完成计算。

这就叫 “分步传参”

二、怎么手写一个通用的柯里化函数?

上面的例子只处理两个参数。现实中,函数可能有 3 个、5 个甚至更多参数。难道我们要手动写 return function(c) { return function(d) {...} } 吗?太麻烦了!

于是,我们需要一个万能工具自动把任意函数变成柯里化版本

// 假设我们有一个四参数函数
function multiply(a, b, c, d) {
  return a * b * c * d;
}

// 手写一个通用的 curry 函数
function curry(fn) {
  return function curried(...args) {
    // 如果当前传入的参数数量 >= 原函数需要的参数数量
    if (args.length >= fn.length) {
      return fn(...args); // 参数齐了,直接执行!
    }
    // 如果还不够,就返回一个新函数,继续收参数
    return function(...nextArgs) {
      return curried(...args, ...nextArgs);
    };
  };
}

// 使用
const curriedMultiply = curry(multiply);

// 以下所有写法都有效!
console.log(curriedMultiply(1)(2)(3)(4));     // 24
console.log(curriedMultiply(1, 2)(3, 4));     // 24
console.log(curriedMultiply(1)(2, 3, 4));     // 24
console.log(curriedMultiply(1, 2, 3, 4));     // 24

🧠 它是怎么工作的?(像讲故事一样)

想象你在点一份“四层蛋糕”:

  1. 第一次你告诉店员:“我要第一层(1)” → 店员记下,但不给你蛋糕,而是说:“请继续告诉我下一层。”
  2. 第二次你说:“第二、三层是 2 和 3” → 店员现在有 [1,2,3],还是不够,继续等。
  3. 第三次你说:“第四层是 4!” → 现在齐了!店员立刻把蛋糕做好给你。

curry 函数就像这个聪明的店员

  • 它用 闭包 记住你之前说的所有层数(参数)
  • 每次你给新层数,它就合并起来
  • 一旦层数够了(args.length >= fn.length),就立刻做蛋糕(执行原函数)

关键技巧:递归 + 闭包 + 剩余参数(...args

三、柯里化到底有什么用?—— 看真实场景

光会写还不够,得知道为什么用。来看一个前端开发中超级常见的需求:打日志

❌ 普通写法(重复、易错):

console.log("[ERROR]: 用户未登录");
console.log("[INFO]: 页面加载完成");
console.log("[WARN]: 图片加载慢");
console.log("[ERROR]: 接口超时");

问题:

  • 每次都要手写 [ERROR][INFO],容易拼错
  • 代码冗长,不好维护

✅ 用柯里化改造:

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("图片加载慢");        // [WARN]: 图片加载慢

🌟 这样做的好处:

好处说明
语义清晰errorLog("xxx") 一看就知道是错误日志
避免重复不用每次都写 "ERROR"
易于复用一个 errorLog 可以用在项目任何地方
方便组合可以直接传给 forEacherrors.forEach(errorLog)

💡 这就像你有三支专用笔:红笔写错误,蓝笔写信息,黄笔写警告——分工明确,效率翻倍!

最后:常见疑问解答

❓ 问:箭头函数 a => b => ... 是什么意思?

答:这是简写!等价于:

function(a) {
  return function(b) {
    ...
  };
}

❓ 问:fn.length 是什么?

答:它是函数定义时的形参个数。比如:

function f(a, b, c) {} 
console.log(f.length); // 3

✅ 总结:柯里化不是炫技,而是工程智慧

🌟 柯里化的本质,是把“一次性配置”和“动态执行”分离,让函数更具弹性与表达力。

虽然Lodash 有 _.curry,Ramda 库默认所有函数都支持柯里化。但理解原理很重要! 在 React Hooks、Redux、Lodash 等现代前端库中,柯里化思想无处不在。掌握它,你就离写出更简洁、更健壮、更函数式的代码更近了一步!

下次当你发现自己在重复传某个固定参数时——就是柯里化登场的时候了!