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