跳到主要内容

聊聊函数柯里化:来源、解决的问题与优缺点

· 阅读需 7 分钟

柯里化(Currying)是函数式编程里最常被提起、也最常被误解的概念之一。面试喜欢问,但很多人只背得住一句「把多参函数变成单参函数的链式调用」,说不清它从哪来、为什么存在、以及它到底划算不划算。

这篇文章把三件事讲透:它是谁发明的它解决什么问题它的代价是什么

一、什么是柯里化

一句话定义:把一个接收多个参数的函数,改造成一串只接收一个参数的函数。

// 普通函数:一次接收 3 个参数
const add = (a, b, c) => a + b + c;

// 柯里化之后:一个一个地收
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6

add(1, 2, 3) 变成 add(1)(2)(3),仅此而已。听着像语法糖,但「每次只收一个参数」背后藏着一整套思想——这就是下一节要说的来源。

二、来源:一段被张冠李戴的数学史

柯里化的根不在 JavaScript,也不在前端,而在数理逻辑

λ 演算:函数天生只能收一个参数

20 世纪 30 年代,Alonzo Church 提出 λ 演算(lambda calculus)——一个只靠「函数定义、函数应用」就能表达一切计算的模型。但 λ 演算里有个硬约束:一个函数只能接收一个参数

那「两个数相加」怎么表示?靠嵌套:

λa.λb.(a + b)

即「一个接收 a、返回『接收 b 的函数』的函数」。多参函数就是单参函数的嵌套——这就是柯里化的数学原型。它不是一个设计选择,而是 λ 演算的生存需要

Schönfinkel 发明,Curry 推广,Strachey 命名

  • Moses Schönfinkel 早在 1924 年就在逻辑学论文里系统阐述了「多参函数可以化为一串单参函数」。
  • Haskell Curry 在 1920–30 年代研究组合子逻辑时独立发展了同样的思想,并把它带进了主流数学逻辑。
  • 「Currying」这个名字,是 Christopher Strachey 在 1967 年造的。有趣的是,它其实是个「错误归因」——真正的发明者是 Schönfinkel,所以这个操作偶尔也被叫 「Schönfinkel 化」(Schönfinkelisation),只是「柯里化」流传更广。

顺带两个冷知识:Haskell 编程语言就是以 Haskell Curry 命名的;Curry 本人的名字还出现在「Curry–Howard 对应」里——证明与程序的同构关系。

从数学走进编程

λ 演算 → ML → Haskell 等函数式语言天然支持柯里化(f a b c 就是 ((f a) b) c)→ JavaScript 社区借鉴过来,但因为 JS 没有原生柯里化,要靠 curry() 这个高阶函数手动转换。

所以柯里化在 JS 里是「借来的概念」,它的很多优点和缺点,都源于这个「借」字。

三、它解决什么问题

1. 参数复用:一个通用函数,变身一堆专用函数

柯里化最大的实际价值是预设参数。把「日志」这个通用函数固定住部分参数,就能派生出专用版:

const log = curry((level, tag, msg) => `[${level}] ${tag}: ${msg}`);

const logError = log('error'); // 固定 level
const logApiError = logError('api'); // 再固定 tag

logApiError('请求超时'); // "[error] api: 请求超时"

不用柯里化的话,你只能写 log('error', 'api', '请求超时'),每次重复前两个参数。柯里化把「重复的样板」收进了一次性的预设里。

2. 延迟执行:攒够参数才跑

柯里化函数不攒够参数就不执行。这让它天然支持「先收集参数、准备好了再触发」:

const fetchData = curry((baseURL, path, params) => request(`${baseURL}${path}`, params));

const api = fetchData('https://api.example.com'); // 还没发请求
const getUser = api('/user'); // 还是没发
getUser({ id: 1 }); // 参数齐了,才真正请求

3. 变成单参函数,好配合高阶函数

mapfilterreduce 的回调都是一元函数(收一个参数)。柯里化能让你把「带配置的运算」直接塞进这些高阶函数:

// 给每个数 +1 —— add(1) 是一元函数,正好喂给 map
[1, 2, 3].map(add(1)); // [2, 3, 4]

// filter 只留大于 2 的
[1, 2, 3].filter(greaterThan(2)); // [3]

这同时为函数组合compose / pipe)铺了路——组合要求「上一个的输出恰好是下一个的输入」,单参函数是标准的积木。

4. 消除重复样板

事件处理、请求配置、格式化……凡是「大部分参数固定、只有最后一两个会变」的地方,柯里化都能把样板收进预设里,调用处只写会变的部分。

四、JS 手写一个 curry

原理很直白:返回一个新函数,收集参数;不够 arity 就继续返回收集函数,够了就调用原函数

function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args); // 参数够了 → 真正执行
}
return (...rest) => curried(...args, ...rest); // 不够 → 攒着,返回新函数
};
}

用法,三种凑参数的方式都行:

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

curriedAdd(1)(2)(3); // 一次一个
curriedAdd(1, 2)(3); // 一次两个
curriedAdd(1)(2, 3); // 先一后二
// 都是 6

注意 fn.length 是函数声明的参数个数,这是「参数够没够」的判断依据——但它不认默认参数和 rest 参数((a, b = 1) => ... 的 length 是 1),这是手写实现的常见坑。

五、优点

优点说明
参数复用固定参数,派生专用函数,减少重复样板
延迟执行攒够参数才执行,天然支持「先配置、后触发」
配合组合输出接输入,一元函数是 compose / pipe 的积木
纯函数、可预测柯里化的每一步都是纯函数,好测试、好推理
语义自文档化log('error') 返回的函数,名字本身就说明了用途

六、缺点

缺点说明
性能开销JS 里每层柯里化都是一个闭包 + 一次函数调用,参数多、调用频繁时有损耗
调用方式不自然f(a)(b)(c) 对命令式思维的开发者不直观,学习曲线陡
可读性下降参数被拆散到多处,函数签名不再一眼看全,(a)(b)(c) 很难从字面读出含义
调试变难错误堆栈里全是嵌套的 curried 调用,定位问题要绕几层
实现有边界fn.length 的坑、this 的绑定、占位符需求,都让手写实现比想象中麻烦
容易过度设计一两次调用也要柯里化,反而把简单事搞复杂

什么时候该用、什么时候别用

适合:函数式风格的项目、参数大量复用、和 pipe / compose 深度配合、配置型 API(固定 baseURL、level 这类)。

不适合:命令式主导的小团队、性能敏感的热点路径(每帧都要跑的函数)、只需要调用一两次的场景——这时 f(a, b, c) 就是最诚实的写法。

七、总结

柯里化从 λ 演算「函数只能收一个参数」的约束里长出来,被 Schönfinkel 发明、Curry 推广、Strachey 命名,然后被 JS 借来用。它解决的问题是参数复用、延迟执行、配合函数组合;它的代价是闭包开销、可读性下降、调试变难。

它不是银弹,而是一个工具——和所有工具一样,好不好用取决于场景。理解了它从哪来、解决什么、付出什么,你才能判断「这个函数要不要柯里化」,而不是因为「函数式的人都在用」而盲目套用。