前言
最近在复习 JavaScript 高阶函数的时候,又把函数柯里化(Currying)翻出来好好捋了一遍。很多人一听到"柯里化"就觉得高大上,其实它没那么神秘,用通俗的话说,就是把一个接受多个参数的函数,变成一个个只接受一个参数的函数链条。
这篇文章就把我自己的学习笔记整理了一下,从最基础的对比开始,慢慢讲到原理、实现、实际用法,希望能帮你把这个知识点彻底吃透。
1. 先看一个最直观的对比
普通写法:
JavaScript
function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 3
柯里化写法:
JavaScript
function add(a) {
return function (b) {
return a + b;
};
}
console.log(add(1)(2)); // 3
看到区别了吗?
- 普通版:一次把所有参数传完。
- 柯里化版:参数一个一个传,每次调用返回一个新函数,直到所有参数收集齐了才真正计算。
这种"一个一个传"的方式,就是函数柯里化的核心。
2. 柯里化的本质:闭包 + 参数收集
为什么能一个一个传?靠的是闭包。
在外层函数里,参数 a 被保存了下来(成了闭包里的自由变量),内层函数可以随时访问它。当我们再传进来 b 的时候,就可以用之前保存的 a 去计算。
所以说,柯里化本质上就是利用闭包把参数"攒"起来,等参数够了再执行真正的逻辑。
3. 怎么判断参数"够了"?
JavaScript 函数有一个隐藏属性 length,它表示函数定义时参数的个数(不包括剩余参数和默认参数)。
JavaScript
function add(a, b) {
return a + b;
}
console.log(add.length); // 2
我们可以利用这个属性来做一个相对严谨的柯里化判断:只有当收集到的参数数量 ≥ 原函数的 length 时,才真正执行。
4. 手写一个通用柯里化函数
下面这个是我自己最常用的一版:
JavaScript
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args); // 直接展开,更清晰
}
return (...more) => curried(...args, ...more); // 这里也用展开合并
};
}
// 测试
function add(a, b, c) {
return 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
这个版本的好处:
- 支持任意数量的参数逐步传递
- 也支持一次传多个(只要总数够了就执行)
- 实现只有十来行,容易理解和记忆
注意:这里用了递归 + 闭包,外层每次调用都会产生新的 curried 函数,args 会被不断累加,直到满足条件。
5. 柯里化的经典实战场景
说了那么多,这么麻烦的编写柯里化函数,它到底能做什么呢?
场景一:固定部分参数,制造专用的工具函数
JavaScript
// 通用日志函数
const log = (type) => (message) => {
console.log(`[${type.toUpperCase()}]: ${message}`);
};
// 通过柯里化"固定"日志类型,得到专用函数
const errorLog = log('error'); // 第一个参数
const infoLog = log('info');
const warnLog = log('warn');
// 第二个参数
errorLog('接口 404 了!'); // [ERROR]: 接口 404 了!
infoLog('页面加载完成'); // [INFO]: 页面加载完成
warnLog('即将弃用旧 API'); // [WARN]: 即将弃用旧 API
这种写法在实际项目里特别常见,尤其是做日志、埋点、事件绑定的时候,能让代码语义更清晰。
场景二:延迟执行 / 参数复用
比如我们有一个通用的 Ajax 请求函数:
JavaScript
function ajax(method, url, data) {
// ...真正的请求逻辑
}
// 柯里化后
const get = curry(ajax)('GET');
const post = curry(ajax)('POST');
const fetchUserList = get('/api/users');
const fetchUserDetail = get('/api/users/');
const submitForm = post('/api/submit');
这样每次调用时就不用反复写 method,代码更简洁,也更不容易写错。
场景三:配合函数式编程库(如 lodash、ramda)
lodash 的 _.curry 功能更强大,支持占位符 __ 来跳过某些参数:
JavaScript
const _ = require('lodash');
const join = (sep, ...arr) => arr.join(sep);
const curryJoin = _.curry(join);
const dotJoin = curryJoin('.');
dotJoin('a', 'b', 'c'); // "a.b.c"
不过日常项目里,自己手写一个简单版往往就够用了。
6. 柯里化的优缺点总结
优点:
- 参数复用:固定前几个参数,快速生成新函数
- 延迟执行:参数没收集齐之前不会真正运行
- 让代码更函数式、更声明式,阅读性更好(尤其配合管道操作)
缺点:
- 产生大量闭包和中间函数,性能略有损耗(现代引擎优化后影响很小)
- 调试时调用栈会变深一点
- 如果滥用,会让代码看起来"太巧妙",反而降低可读性
所以我的建议是:合适的地方用,别为了柯里化而柯里化。
最后
函数柯里化其实就是一个很小的技巧,但用好了能让你的代码更优雅、更灵活。尤其是当你开始接触函数式编程、React 高阶组件、Redux 中间件这些场景时,会发现柯里化的影子到处都是。
希望这篇从零开始的整理,能帮你把柯里化彻底搞明白。欢迎在评论区分享你用柯里化写过的有趣代码,或者你踩过的坑~