在此之前的很多次面试里,你可能都遇到过这样一个经典的"送命题":
"请实现一个
add函数,使得add(1)(2)(3)的结果等于 6。"
乍一看,这像是面试官在故意刁难。毕竟在正常的业务开发里,谁没事会把参数拆得七零八落?但其实,这背后藏着一个函数式编程中非常性感且实用的概念------柯里化 (Currying) 。
今天我们就抛开那些晦涩的数学定义,用大白话和最骚的代码,彻底搞懂柯里化到底是个什么鬼,以及它为什么能让你的代码更优雅。
🧐 什么是柯里化?从"一口闷"到"细嚼慢咽"
在传统的函数调用中,我们习惯"一口闷"。
比如一个求和函数 function add(a, b, c) { return a + b + c },我们要么一次性把 a, b, c 全传进去,要么就报错。
柯里化的本质,就是把"一次性传参"变成"一个个传参" 。
如果说普通函数是 $f(a, b, c)$,那么柯里化后的函数就是 $f(a)(b)(c)$。
核心心法:闭包是仓库,递归是搬运工
柯里化之所以能实现,全靠 JavaScript 的两个老朋友:
- 闭包 (Closure) :每一层函数都像一个"小仓库",记住了之前传进来的参数,这些变量(自由变量)不会被销毁,而是静静地待在那里。
- 递归 (Recursion) :只要参数还没凑够,我就继续返回一个新的函数,等着你传下一个参数;一旦凑够了,直接"召唤神龙",执行原函数。
🛠 手撸一个通用的 Curry 函数
光说不练假把式。我们来看看,如何把任何一个普通函数,变成柯里化函数。
1. 知道什么时候"够了"
首先,我们需要知道原函数到底需要几个参数。在 JS 里,函数有一个属性 length,它代表了函数形参的个数。
JavaScript
javascript
function add(a, b, c, d) {
return a + b + c + d;
}
console.log(add.length); // 输出 4 -> 这就是我们的目标数量
2. 实现核心逻辑
接下来,我们写一个 curry 辅助函数。它的逻辑非常简单粗暴:参数没收齐,我就一直返回函数;收齐了,我就执行。
JavaScript
php
/**
* 柯里化通用实现
* @param {Function} fn - 需要被柯里化的原函数
*/
function curry(fn) {
// 这里的 ...args 是闭包这层仓库里已经存好的参数
return function curried(...args) {
// 【核心判断】:当前收集的参数数量 >= 原函数需要的参数数量?
if (args.length >= fn.length) {
// 🚪 退出条件:参数够了,直接执行原函数
return fn(...args);
} else {
// 🔄 递归继续:参数不够?返回一个新的函数继续收参数
return (...rest) => {
// 将之前存的 args 和新传进来的 rest 合并,递归调用 curried
return curried(...args, ...rest);
};
}
}
}
3. 见证奇迹的时刻
现在我们用这个 curry 函数来包装一下我们的 add:
JavaScript
scss
const curriedAdd = curry(add);
// 玩法 1:标准柯里化,一次传一个
console.log(curriedAdd(1)(2)(3)(4)); // 10
// 玩法 2:不严谨柯里化,一次传多个(更灵活!)
console.log(curriedAdd(1, 2)(3)(4)); // 10
console.log(curriedAdd(1, 2, 3, 4)); // 10
你看,无论你怎么传,只要总数凑够了 4 个,它就会给你吐出结果。这就是闭包在背后默默负重前行的结果。
💡 为什么我要用它?(实战场景)
你可能会问:"这就这就?除了面试装X,这有啥用?"
柯里化最大的价值在于:参数复用(偏函数应用)与 延迟执行。
场景:打造优雅的日志系统
假设你有一个通用的日志打印函数:
JavaScript
typescript
// 通用日志函数
const log = (type, message) => {
console.log(`[${type}]: ${message}`);
}
在实际业务中,你要打印很多 ERROR 类型的日志。如果不柯里化,你得这么写:
JavaScript
arduino
log("ERROR", "接口挂了");
log("ERROR", "数据不对");
log("ERROR", "服务器着火了");
太 啰 嗦 了! ERROR 这个参数我们重复写了三遍。
利用柯里化(或者在这里更准确说是偏函数应用的思想),我们可以"固定"住第一个参数:
JavaScript
typescript
// 这里我们用箭头函数简写一个针对 log 的柯里化版本
const curriedLog = type => message => {
console.log(`[${type}]: ${message}`);
}
// 🏭 批量生产专用函数
const errorLog = curriedLog("ERROR"); // 固定住了 "ERROR"
const infoLog = curriedLog("INFO"); // 固定住了 "INFO"
// ✅ 现在的调用:语义更清晰,代码更简洁
errorLog("接口挂了"); // [ERROR]: 接口挂了
infoLog("页面加载完成"); // [INFO]: 页面加载完成
这一波操作下来,代码的可读性(errorLog 一眼就知道是干嘛的)和复用性(不用重复传 type)都得到了极大的提升。
📝 总结一下
- 柯里化 (Currying) 是一种将多参数函数转换为一系列单参数函数的技术。
- 实现原理 :利用 闭包 保存历史参数,利用 递归 收集参数,利用
fn.length判断退出时机。 - 应用场景:当你需要频繁调用一个函数,且其中某些参数是固定的时候,柯里化能帮你通过"预置参数"来减少重复代码,提升代码的语义化。
下次再看到 add(1)(2)(3),别再觉得它是花拳绣腿了。它是 JavaScript 函数式编程中一把精致的手术刀,用得好,你的代码就能像诗一样优雅。✨