前言:嘿,哥们儿,柯里化这玩意儿你听过没?
在咱们 JavaScript 的江湖里,函数可不是那种“干完活就走”的工具人,它们可是能跑能跳、能传能回的“一等公民”!正因为这份自由,才诞生了好多酷炫的编程玩法,其中一个听起来有点“高大上”但实际上超好用的,就是“柯里化”(Currying)!
你是不是觉得这名字有点拗口?别担心,咱们今天就把它扒个精光,让你彻底明白这货是干嘛的,为啥面试官老爱问它,以及它能怎么帮我们写出更“香”的代码!
说真的,搞懂柯里化,不光能让你在面试时“秀”一把,还能让你写代码的时候感觉自己像个魔法师,把复杂的东西变得简单又优雅。来来来,搬好小板凳,咱们一起揭秘柯里化的魔法!
柯里化是啥?—— 简单说,就是把“一口吃个胖子”变成“细嚼慢咽”!
柯里化这概念,说白了就是把一个需要好几个参数的函数,拆分成一系列每次只接受一个参数的函数。每次你给它一个参数,它就给你返回一个新的函数,直到所有参数都到齐了,它才“吭哧吭哧”地把最终结果吐出来。
是不是有点像你点了一份豪华套餐,服务员不是一次性把所有菜都端上来,而是每上一个菜,都问你“还要点别的吗?”直到你把所有点的菜都吃完了,才给你上甜点?哈哈,就是这个意思!
传统函数调用: —— 咱们最熟悉的“一口闷”
先看个最最普通的加法函数,这谁不会啊?
// c:\Users\MR\Desktop\workspace\lesson_jp\js\curry\1.js
function add(a, b){
return a + b;
}
// 参数一个个传递
console.log(add(1, 2)); // 输出:3
这多直接啊,add 函数要 a 和 b,我一次性给它,它立马就算出结果。简单粗暴,效率高!但有时候,咱们就想玩点花样,比如……
初探柯里化: —— 第一次尝试“细嚼慢咽”
如果我想让 add 函数变成 add(1)(2) 这样调用,是不是感觉有点意思了?这就是柯里化的雏形!
// c:\Users\MR\Desktop\workspace\lesson_jp\js\curry\2.js
function add(a) {
return function(b){
return a + b;
}
}
// 函数柯理化 add(1)(2)
console.log(add(1)(2)); // 输出:3
瞧瞧这代码,是不是有点小惊喜?
add(1)这一步,它没直接算加法,而是“变”出了一个新函数!- 这个新函数可聪明了,它“记住”了你给的第一个参数
a(也就是1)。 - 等到你再调用
(2)的时候,它就把之前记住的a和现在给的b凑一块儿,算出最终结果!
这种“记住”外面变量的超能力,就是咱们 JavaScript 里大名鼎鼎的闭包!是不是感觉闭包瞬间亲切多了?它就是柯里化的小助手,帮它把参数一个个“存”起来!
柯里化的核心魔法:闭包和递归的“双剑合璧”!
从上一个的小例子里,咱们已经看到了闭包的影子。但如果一个函数有三四个、甚至更多参数,难道我们要手动套娃套好几层吗?那也太累了吧!所以,我们需要一个“万能柯里化机器”,能自动帮我们搞定这事儿。而这台机器的核心动力,就是闭包和递归这对黄金搭档!
闭包:参数收集的“小金库”
- 柯里化的本质是闭包 每一层函数 都接受自己的参数, 当参数数量 到位后,执行函数
- 闭包中的自由变量一直在,不会销毁,用于不断收集参数(递归中重复的事情)
闭包就像一个贴心的小管家,它能让内部函数“记住”外部函数作用域里的变量,就算外部函数都执行完了,这些变量也还在那儿,不会消失!在柯里化里,这意味着每次返回的新函数,都能访问到之前你给的参数。这些参数就像宝贝一样,被闭包牢牢地“收藏”起来,就等着所有参数都到齐的那一刻,然后“哗啦”一下,把结果变出来!
递归:参数收集的“永动机”(直到条件满足)
柯里化就是个不断收集参数的过程。参数不够?没关系,我再给你变个新函数,你继续给!参数够了?好嘞,那我就把最终结果给你!这种“不够就继续,够了就收手”的重复模式,就是递归的拿手好戏!
- 退出条件 参数数量到位 执行函数
递归在这里就像一个“智能导航”,它不断地“问路”(返回新函数),直到它发现“目的地到了”(所有参数都收集齐了),然后就停止问路,直接带你到终点!
怎么造一个“万能柯里化机器”?—— 拆解例题的秘密!
好啦,现在咱们要来个硬核的了!看看怎么写一个通用的柯里化函数,让它能柯里化任何参数数量的函数!
// c:\Users\MR\Desktop\workspace\lesson_jp\js\curry\3.js
function add(a ,b,c,d){
return a + b + c + d;
}
console.log(add.length);// 获得函数需要的参数数量
function curry(fn){
// closure curry fn
// curried args 收集参数 不那么严谨的柯理化函数
return function curried(...args){
// curried 闭包
if(args.length >= fn.length){
return fn(...args); // 退出:当收集的参数数量达到原始函数所需,执行原始函数
}
// 递归:返回一个新函数,继续收集参数
// ...rest 收集当前次调用传入的参数
// curried(...args,...rest) 将之前收集的参数和当前参数合并,再次调用 curried
return (...rest) => curried(...args,...rest);
}
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)(4)); // 输出:10
console.log(curriedAdd(1, 2)(3)(4)); // 输出:10
console.log(curriedAdd(1, 2, 3, 4)); // 输出:10
console.log(curriedAdd(1)(2, 3)(4)); // 输出:10
这段代码就是咱们的“万能柯里化机器”!咱们来一层层地揭开它的面纱!
fn.length:这个小属性,是判断“够不够”的关键!
function add(a ,b,c,d){
return a + b + c + d;
}
console.log(add.length);// 获得函数需要的参数数量 // 输出:4
在 JavaScript 里,每个函数都有个 length 属性,它会告诉你这个函数“明面上”需要多少个参数(那些 ...rest 和默认参数不算哦)。这个 length 就是咱们判断“参数够不够”的秘密武器!比如 add(a, b, c, d),它的 add.length 就是 4。
curry 函数:柯里化机器的“外壳”
function curry(fn){
// ...
return function curried(...args){
// ...
}
}
curry 函数本身是个“高阶函数”(就是能接收函数做参数,或者返回函数的函数)。它接收你想要柯里化的原始函数 fn,然后返回一个全新的函数 curried。这个 curried 函数,就是咱们柯里化后的“新版本”!
curried 内部函数:魔法发生的地方!
curried 函数才是真正施展魔法的地方,它把闭包和递归玩得溜溜的!
1. 参数够了没?args.length >= fn.length
if(args.length >= fn.length){
return fn(...args); // 退出:当收集的参数数量达到原始函数所需,执行原始函数
}
这是咱们递归的“刹车条件”!args 数组里装着你目前为止给的所有参数。如果 args.length(已收集参数总数)已经大于等于 fn.length(原始函数所需参数数),那就说明“菜都上齐了”!这时候,咱们就直接把这些参数 ...args 喂给原始函数 fn,让它干活,然后把结果吐出来!
2. 参数还不够?继续收集!curried(...args,...rest)
// 递归:返回一个新函数,继续收集参数
// ...rest 收集当前次调用传入的参数
// curried(...args,...rest) 将之前收集的参数和当前参数合并,再次调用 curried
return (...rest) => curried(...args,...rest);
如果 args.length 还没达到 fn.length,那说明“菜还没上齐呢”!这时候 curried 函数不会急着干活,而是又“变”出一个新的匿名函数!
这个新函数也挺能干,它会接收你这次给的参数(...rest)。然后,它会把之前收集的 args 和这次新给的 rest 合并起来,再递归地调用 curried 函数自己,把合并后的参数列表传进去!
看明白了吗?curried 函数就像个“记忆大师”,它通过闭包记住 fn 和 fn.length,还不断地把 args 这个参数列表“养肥”。每次你调用它,它就看看参数够不够,不够就继续“变”出新函数来收集,直到参数够了,就一锤定音,执行原始函数!是不是很酷?
完整代码,加点“唠嗑式”注释!
为了让你看得更明白,咱们再把 3.js 的代码搬出来,这次加上更多“大白话”注释!
// 原始函数:一个需要四个参数的加法函数,咱们的目标就是把它柯里化!
function add(a ,b,c,d){
return a + b + c + d;
}
// 看看原始函数“明面上”需要几个参数?
console.log(add.length); // 输出:4 (嗯,它需要4个参数才能干活)
/**
* 柯里化魔法师!
* @param {Function} fn - 你想柯里化的那个原始函数
* @returns {Function} - 柯里化后的“新函数”,可以分批喂参数啦!
*/
function curry(fn){
// curry 函数会返回一个叫 curried 的函数。
// curried 就是那个“小管家”,它能通过闭包记住外面的 fn 和 fn.length。
// ...args 就像一个“篮子”,用来装你每次给它的参数。
return function curried(...args){
// 核心判断:篮子里的参数够不够原始函数 fn 吃的?
// fn.length 就是 fn 期望的参数数量。
if(args.length >= fn.length){
// 够了!够了!那就别藏着掖着了,直接把篮子里的参数 ...args 喂给 fn,让它干活!
// 这就是递归的“终点站”!
return fn(...args);
}
// 还没够?没关系,我再给你变个新函数!
// 这个新函数 (...rest) => ... 会接收你这次给的参数(可以是好几个)。
// 然后,它会把之前篮子里的 args 和这次新给的 rest 合并起来,
// 再“召唤” curried 函数自己(递归!),把合并后的参数再扔给它!
// 这样,参数就会一点点地累积起来,直到够数为止!
return (...rest) => curried(...args,...rest);
}
}
// 用咱们的柯里化魔法师,把 add 函数变身!
const curriedAdd = curry(add);
// 看看变身后的 curriedAdd 怎么玩:
// 1. 一个个喂参数,就像吃薯条一样,一根一根来!
console.log(curriedAdd(1)(2)(3)(4)); // 输出:10
// 2. 也可以一次喂几个,再一个个喂,灵活得很!
console.log(curriedAdd(1, 2)(3)(4)); // 输出:10
// 3. 甚至你也可以一次性把所有参数都喂给它,它也照样能干活!
console.log(curriedAdd(1, 2, 3, 4)); // 输出:10
// 4. 各种花式喂法,它都能搞定!
console.log(curriedAdd(1)(2, 3)(4)); // 输出:10
是不是感觉柯里化瞬间变得亲民多了?它就是闭包和递归这对好基友,一起玩转参数收集的游戏!
柯里化有啥用?—— 让你的代码更“香”更“有范儿”!
柯里化可不是光用来面试装X的,它在实际开发中也有很多超实用的场景,能让你的代码变得更优雅、更灵活、更好维护!
日志系统: —— 告别重复,定制你的专属日志!
日志系统咱们都用过吧?错误日志、信息日志、警告日志……每次打印日志都要写 log('error', 'xxx'),是不是有点烦?柯里化就能帮你解决这个小烦恼!
// 日志函数,它本身就是柯里化的形式!
const log = type => message => {
console.log(`[${type}]: ${message}`);
}
// 柯里化,就是把参数“固定”住,让函数更有“个性”!
// 固定日志类型,让函数有自己的“语义”
const errorLog = log('error'); // 专门记录错误的日志函数
const infoLog = log('info'); // 专门记录信息的日志函数
errorLog('接口异常'); // 输出:[error]: 接口异常 (看,现在只需要传消息就行了!)
infoLog('查询成功'); // 输出:[info]: 查询成功 (是不是清爽多了?)
以前写日志,可能有点“啰嗦”
如果不用柯里化,你可能会这样写:
function logMessage(type, message) {
console.log(`[${type}]: ${message}`);
}
logMessage('error', '接口异常');
logMessage('info', '查询成功');
logMessage('warn', '数据格式不正确');
每次都要重复写 type,感觉有点像在说废话,对吧?
柯里化让日志函数“脱胎换骨”!
在实际开发里,log 函数本身就是个柯里化的“好手”:
const log = type => message => {
console.log(`[${type}]: ${message}`);
}
log函数先收type参数,然后“变”出一个新函数。- 这个新函数再收
message参数,最后才把日志打出来。
这样一来,咱们就能固定住 type 参数,定制出各种专属日志函数:
const errorLog = log('error'); // errorLog 现在就是你的“错误日志专属打印机”!
const infoLog = log('info'); // infoLog 则是你的“信息日志专属打印机”!
这么做有啥好处呢?
- 参数不用老重复:
'error'和'info'这些参数,你只需要写一次,后面就不用再操心了。 - 代码读起来更舒服:
errorLog('接口异常')比logMessage('error', '接口异常')是不是更直观,一眼就知道这是在记错误日志? - 代码更简洁:调用的时候,参数少了,代码自然就短了,看起来也更清爽!
参数复用和“慢悠悠”执行
柯里化还有一个超赞的特点,就是能复用参数!如果你的某个函数,老是需要用同一个参数,那柯里化就能帮你把这个参数“预设”好,然后给你一个“定制版”的函数。
比如,有个计算折扣的函数 calculateDiscount(rate, price)。如果你有 9 折、8 折这种固定折扣,就可以柯里化它:
const discount = rate => price => price * rate;
const nineDiscount = discount(0.9); // 9折专属函数
const eightDiscount = discount(0.8); // 8折专属函数
console.log(nineDiscount(100)); // 90
console.log(eightDiscount(200)); // 160
你看,rate 参数是不是被完美复用了?而且,discount(0.9) 并没有马上算折扣,它只是返回了一个“等着你给价格”的函数。只有当你把 price 传进去之后,最终的计算才会发生。这种“不着急,等我准备好了再干活”的特性,就叫延迟执行!
函数组合:把小函数拼成大招!
柯里化还是函数式编程里实现函数组合(Function Composition)的得力助手!函数组合就像搭乐高,把一个个小功能函数拼起来,变成一个更强大的函数。前一个函数的输出,直接作为后一个函数的输入。
比如,我们有两个小函数:add1(x) = x + 1 和 multiply2(x) = x * 2。我们想先加 1 再乘 2,也就是 multiply2(add1(x))。
const add1 = x => x + 1;
const multiply2 = x => x * 2;
// 柯里化后的 compose 函数(虽然 compose 本身不一定是柯里化,但它爱和柯里化函数玩)
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const add1ThenMultiply2 = compose(multiply2, add1);
console.log(add1ThenMultiply2(5)); // (5 + 1) * 2 = 12
柯里化函数特别适合和这种组合函数一起玩,因为它们通常只接受一个参数,或者可以被柯里化成只接受一个参数的形式,这样组合起来就特别顺滑!
柯里化:优点和缺点,咱们也得聊聊!
任何技术都有两面性,柯里化也不例外。
优点(让你爱上它!)
- 参数复用:这绝对是它的杀手锏!能帮你创建出各种“定制版”函数,减少重复代码,让你的代码更 DRY (Don't Repeat Yourself)。
- 延迟执行:函数不会急着干活,而是等所有条件都满足了才动手。这在处理事件、异步操作时特别有用。
- 代码更清晰,模块化更强:通过创建有明确语义的函数,让你的代码读起来更像自然语言,也更容易拆分成小模块。
- 函数组合的好搭档:和
compose、pipe这些函数式工具一起用,简直是天作之合,能帮你构建出超复杂的逻辑流。 - 测试更简单:因为函数职责更单一,每次只处理一个参数,所以写单元测试的时候也更轻松。
缺点(咱们也得知道!)
- 调用层级变多:从
f(a, b, c)变成f(a)(b)(c),调用链确实长了点。刚开始接触的同学可能会觉得有点绕。 - 性能开销:每次柯里化调用都会创建新的函数和闭包,这会消耗一些内存和 CPU。对于那些对性能要求极高的场景,可能需要稍微权衡一下。
- 调试可能有点小麻烦:多层嵌套的函数和闭包,在调试的时候,调用栈可能会深一点,找问题可能要多费点劲。
fn.length的小陷阱:咱们柯里化依赖fn.length来判断参数数量,但它不包括...rest参数和默认参数。如果你的原始函数用了这些,那柯里化的实现可能需要稍微调整一下,不然可能会“算错”参数数量哦!
总结:柯里化,你值得拥有!
柯里化在 JavaScript 函数式编程里,绝对是个“宝藏”概念!它巧妙地利用了闭包来“记住”参数,又借助递归来控制参数收集的节奏。
学会柯里化,不光能让你在面试中“闪闪发光”,还能让你的日常开发变得更轻松、更愉快!从日志系统到参数复用,再到函数组合,柯里化都能给你带来意想不到的惊喜。
当然啦,技术没有银弹,柯里化也不是万能的。在享受它带来的便利时,也要留意它可能带来的那么一丢丢性能和调试上的小挑战。只有真正理解了它的原理和适用场景,你才能把它玩得炉火纯青,让你的 JavaScript 代码“香”飘万里!
希望这篇柯里化文章,能让你彻底爱上它!快去你的代码里试试柯里化的魔法吧!