什么是函数柯里化?原理与实战

什么是函数柯里化?原理与实战

“函数柯里化(Currying)”是函数式编程中的经典概念,也是前端面试/源码中常见的技巧。很多人对它的第一印象是:“就是把多参数函数,拆成多个一参函数”,但实际应用远不止于此。

本文围绕以下几个问题展开:

  • 柯里化的严格定义是什么?
  • 为什么要柯里化?它能解决什么问题?
  • 如何手写一个通用的 curry 函数?
  • 在前端业务与框架源码中,它有哪些常见用法?

一、函数柯里化的定义

形式上的定义:

柯里化就是将一个“接受多个参数”的函数,转换成“一连串每次只接受一个参数”的函数的技术。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(x, y) {
return x + y;
}

// 柯里化后
function addCurried(x) {
return function (y) {
return x + y;
};
}

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

更一般地,一个 f(a, b, c) 可以被转换为 f(a)(b)(c)


二、为什么要柯里化?

柯里化的几个典型用途:

  1. 参数复用:先“锁定”一部分参数,得到一个“更具体”的函数。
  2. 延迟执行:通过多次函数调用的方式,延迟到最后一次传入参数才真正执行逻辑。
  3. 提升函数组合能力:更易于函数式编程风格的组合与管道。

1. 参数复用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function logger(level, module, message) {
console.log(`[${level}] [${module}] ${message}`);
}

// 柯里化后
function loggerCurried(level) {
return function (module) {
return function (message) {
console.log(`[${level}] [${module}] ${message}`);
};
};
}

const infoLog = loggerCurried("INFO");
const userInfoLog = infoLog("USER");

userInfoLog("登录成功");
userInfoLog("退出登录");

通过柯里化,我们可以先“固化”日志等级,再“固化”模块名,最后在业务代码中只关注消息本身。


三、手写一个简单 curry 函数

目标:

1
2
3
4
5
6
7
8
9
function add(a, b, c) {
return a + b + c;
}

const curriedAdd = curry(add);

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

1. 基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function curry(fn) {
const len = fn.length; // 形参数量

function curried(...args) {
if (args.length >= len) {
return fn.apply(this, args);
} else {
return function (...rest) {
return curried.apply(this, args.concat(rest));
};
}
}

return curried;
}

要点:

  • 通过 fn.length 获取原函数期望的参数个数
  • 递归收集参数,直到收集的参数个数 >= 期望值
  • 每次返回一个新函数,闭包中记住已传入的参数

四、进阶:支持不定参数与 placeholder

像 lodash 的 _.curry 甚至支持占位符(placeholder),例如:

1
2
3
4
const _ = {};
const fn = curry((a, b, c) => [a, b, c]);

fn(_, 2, 3)(1); // [1, 2, 3]

完整实现较复杂,这里给出一个简化版思路:

  • 使用特定符号(如 _)代表“尚未提供的参数”
  • 在每次调用时,按顺序填补 placeholder
  • 直到所有参数都被非 placeholder 填满时,真正执行函数

这种写法在大型函数式库或工具函数库中更常见,日常业务中使用频率相对较低。


五、柯里化在前端中的实际应用

1. 配置化事件处理

1
2
3
4
5
6
7
8
9
10
function handleChange(field) {
return function (event) {
const value = event.target.value;
setForm((prev) => ({ ...prev, [field]: value }));
};
}

// 使用
<input onChange={handleChange("username")} />
<input onChange={handleChange("password")} />

这里 handleChange 就是一个“手动柯里化”的函数:

  • 第一次调用传入字段名
  • 返回的函数在事件触发时再接收 event

2. 日志与埋点

1
2
3
4
5
6
7
8
9
const track = curry((category, action, label, value) => {
// 上报埋点
});

const trackUser = track("user");
const trackUserLogin = trackUser("login");

trackUserLogin("success", 1);
trackUserLogin("fail", 0);

通过柯里化,可以逐步固化部分上下文,让最终的埋点上报函数在业务处更简洁。

3. 函数组合(compose / pipe)

在函数式编程中,柯里化常与组合函数一起使用:

1
2
3
4
5
6
7
const map = curry((fn, arr) => arr.map(fn));
const filter = curry((fn, arr) => arr.filter(fn));

const getActiveUserNames = pipe(
filter((u) => u.active),
map((u) => u.name)
);

这类写法在 Ramda 等函数式库中非常常见。


六、柯里化与偏函数(Partial Application)的区别

两者容易混淆:

  • 柯里化:严格地将多参函数转化为一串单参函数;
  • 偏函数(Partial Application):固定函数的某些参数,返回一个“参数更少”的新函数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
function sum(a, b, c) {
return a + b + c;
}

// 偏函数:固定前两个参数
function partialSum(a, b) {
return function (c) {
return sum(a, b, c);
};
}

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

偏函数不要求每次只接收一个参数,而柯里化在数学上通常定义为“一元函数链”。

在前端实践中,两者常常被混用,重点在于:

  • 通过多次调用,逐步“固化部分参数”,提升复用性与可读性。

七、何时应该(或不该)使用柯里化?

适合使用的场景:

  • 参数复用明显(如日志、埋点、通用校验)
  • 组合函数、函数式管道风格
  • API 设计希望更“声明式”

不适合滥用的场景:

  • 业务同学阅读成本较高的代码区域
  • 性能极度敏感且函数被高频调用的热路径(频繁创建闭包)

建议:

  • 在公共工具库中适度引入柯里化,配好注释与类型(TS)
  • 在业务逻辑中看场景使用,避免为了“炫技”增加理解难度

八、总结

函数柯里化可以理解为:

  • 从接口设计角度:允许你“分次传参”,在不同阶段复用一部分上下文;
  • 从实现角度:通过闭包保存已传入的参数,在参数凑齐时真正执行原函数;
  • 从应用角度:在事件处理、埋点、日志、函数式组合等场景中非常实用。

掌握柯里化,不仅能帮你写出更优雅的函数工具,也能够在阅读诸如 lodash、Ramda 等库源码时更加游刃有余。


什么是函数柯里化?原理与实战
https://sunjc.vip/2024/08/11/什么是函数柯里化/
作者
Sunjc
发布于
2024年8月11日
许可协议