🍛 JavaScript 柯里化:把“大餐”拆成“小炒”的艺术
在函数式编程中,柯里化(Currying)是一个高频词汇。
很多初学者看到类似add(1)(2)(3)这样的代码时会一头雾水:为什么函数可以这样调用?它到底有什么用?
别急,今天我们就把这个看似复杂的概念,嚼碎了喂给你。
📂 目录
- 🤔 什么是柯里化?
- 🥘 生活化比喻:从“满汉全席”到“自助套餐”
- 💻 代码对比:普通函数 vs 柯里化函数
- 🛠️ 手写一个通用的柯里化工具函数
- 🚀 柯里化的三大实战场景
- 💡 总结
1. 🤔 什么是柯里化?
官方定义:
柯里化(Currying)是把接受多个参数的函数,变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
通俗解释:
原本你需要一次性给函数所有参数,它才干活。
柯里化之后,你可以分批给参数。每给一个参数,它就返回一个新函数,等着接收下一个参数,直到所有参数都给齐了,它才最终执行并返回结果。
核心特征:
- 参数分批传递:
fn(a, b, c)变成fn(a)(b)(c)。- 延迟执行:参数没给够之前,不执行具体逻辑,只返回新函数。
- 参数复用:固定的参数可以先传进去,生成一个“半成品”函数供后续使用。
2. 🥘 生活化比喻:从“满汉全席”到“自助套餐”
想象你去餐厅吃饭:
❌普通函数:
你必须一次性点完所有菜(主食、饮料、甜点),厨房才开始做。如果你忘了点饮料,就得重新下单,或者等下次再来。cook("牛排", "可乐", "冰淇淋")✅柯里化函数:
你采用自助餐模式。- 你先拿了一个盘子,放了牛排。(返回一个新状态:
盘子里有牛排) - 接着,你又拿了可乐。(返回一个新状态:
盘子里有牛排+可乐) - 最后,你拿了冰淇淋。(此时盘子满了,厨房开始制作套餐,端给你。)
cook("牛排")("可乐")("冰淇淋")- 你先拿了一个盘子,放了牛排。(返回一个新状态:
好处是什么?
如果你每天都吃牛排,你可以先预置一个“牛排盘”,每天只需要决定加什么饮料和甜点即可。这就是“参数复用”!
3. 💻 代码对比:普通函数 vs 柯里化函数
假设我们要实现一个简单的加法函数。
❌ 普通函数
functionadd(x,y,z){returnx+y+z;}// 必须一次性传入所有参数console.log(add(1,2,3));// 6✅ 柯里化函数
functioncurriedAdd(x){returnfunction(y){returnfunction(z){returnx+y+z;};};}// 可以分批传入参数conststep1=curriedAdd(1);// 返回一个函数,等待接收 yconststep2=step1(2);// 返回一个函数,等待接收 zconstresult=step2(3);// 所有参数齐了,执行计算,返回 6// 或者链式调用console.log(curriedAdd(1)(2)(3));// 6看起来代码变多了?是的,手动写柯里化很麻烦。所以我们需要一个通用的工具函数来自动完成这个转换。
4. 🛠️ 手写一个通用的柯里化工具函数
在实际开发中,我们不会为每个函数都手动嵌套闭包。我们会编写一个高阶函数curry,它能把任何普通函数转换成柯里化函数。
💻 实现代码
/** * 通用柯里化函数 * @param {Function} fn - 需要被柯里化的原函数 * @returns {Function} - 柯里化后的新函数 */functioncurry(fn){// 获取原函数期望的参数个数constarity=fn.length;returnfunctionjudge(...args){// 如果当前收集的参数个数 >= 原函数期望的参数个数if(args.length>=arity){// 执行原函数,返回结果returnfn.apply(null,args);}else{// 否则,返回一个新函数,继续收集剩余参数returnfunction(...restArgs){// 递归调用 judge,将已收集的参数和新参数合并returnjudge.apply(null,args.concat(restArgs));};}};}🧪 测试一下
// 1. 定义一个普通函数functionsum(a,b,c){returna+b+c;}// 2. 将其柯里化constcurriedSum=curry(sum);// 3. 各种调用方式均有效console.log(curriedSum(1,2,3));// 6 (一次性传完)console.log(curriedSum(1)(2,3));// 6 (分批传)console.log(curriedSum(1)(2)(3));// 6 (完全柯里化)// 4. 参数复用示例constadd10=curriedSum(10);console.log(add10(20,30));// 60 (10 + 20 + 30)console.log(add10(1,2));// 13 (10 + 1 + 2)5. 🚀 柯里化的三大实战场景
柯里化不仅仅是炫技,它在实际开发中有巨大的价值。
场景一:参数复用(固定配置)
这是柯里化最核心的用途。当你有一个函数,其中某些参数是固定的,只有少数参数变化时,柯里化可以帮你创建一个“专用版本”的函数。
例子:正则验证
// 普通写法:每次都要传正则表达式functioncheck(reg,txt){returnreg.test(txt);}check(/\d+/g,'test');// falsecheck(/[a-z]+/g,'test');// true// 柯里化写法:预先固定正则,生成专用验证器constcurriedCheck=curry(check);consthasNumber=curriedCheck(/\d+/g);consthasLetter=curriedCheck(/[a-z]+/g);// 后续使用极其简洁,且语义清晰hasNumber('test123');// truehasLetter('123');// false场景二:延迟执行
在某些场景下,我们希望函数先接收部分参数,但不立即执行,直到满足特定条件或收集完所有参数后再执行。
例子:日志打印
functionlog(level,date,msg){console.log(`[${level}]${date}:${msg}`);}constcurriedLog=curry(log);// 预设级别和时间constinfoLog=curriedLog('INFO')(newDate().toLocaleDateString());// 在代码的不同地方,只关心消息内容infoLog('用户登录成功');infoLog('数据加载完成');场景三:兼容性处理与函数组合
在函数式编程库(如 Lodash、Ramda)中,柯里化是函数组合(Compose)的基础。只有当函数都是单参数(或柯里化后表现为单参数流)时,才能像管道一样轻松串联。
// 假设我们有 lodash 的 curry 和 flowimport{curry,flow}from'lodash';constadd=curry((a,b)=>a+b);constmultiply=curry((a,b)=>a*b);// 组合函数:先加 10,再乘 2constadd10ThenMultiply2=flow(add(10),multiply(2));console.log(add10ThenMultiply2(5));// (5 + 10) * 2 = 30💡 总结
| 特性 | 普通函数 | 柯里化函数 |
|---|---|---|
| 参数传递 | 一次性传递所有参数 | 分批传递,一次一个(或一组) |
| 执行时机 | 调用即执行 | 参数凑齐后才执行(延迟执行) |
| 主要优势 | 简单直观 | 参数复用、延迟执行、易于组合 |
| 适用场景 | 大多数常规业务逻辑 | 配置项固定、函数式编程、高阶组件封装 |
🚀 博主寄语:
柯里化本质上是一种**“降维打击”**的思维:
它将一个多参问题,分解为多个单参问题的序列。不要为了柯里化而柯里化。
当发现你在重复传递相同的参数时,就是使用柯里化的最佳时机。记住口诀:
多参变单参,
闭包来帮忙。
参数若不够,
返回新函数。
参数若凑齐,
执行出结果。
复用与延迟,
代码更优雅。
希望这篇文档能帮你彻底掌握柯里化!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦!❤️