大家好!今天我们来聊聊JavaScript中一个既神秘又有趣的概念——柯里化(Curry)。这可不是咖喱(Curry)那个好吃的食物哦,而是一种强大的函数式编程技术!
一、闭包:柯里化的基石
在深入柯里化之前,我们需要先理解闭包(Closure)这个概念。闭包就像是函数的小背包,可以随身携带一些"零食"(自由变量)。
function outer() {
const snack = '🍪'; // 自由变量
return function inner() {
console.log(`我偷偷吃了个${snack}`);
};
}
const eatSnack = outer();
eatSnack(); // 输出: 我偷偷吃了个🍪
这里inner函数就是一个闭包,它可以访问外层函数的snack变量,即使outer已经执行完毕。这个特性正是柯里化能够实现的关键!
二、函数是一等公民
在JavaScript中,函数是一等公民(first-class citizen),这意味着:
- 函数可以赋值给变量
- 函数可以作为参数传递
- 函数可以作为返回值
// 1. 赋值给变量
const sayHi = function() { console.log('Hi!'); };
// 2. 作为参数传递
function greet(fn) {
fn();
}
greet(sayHi);
// 3. 作为返回值
function createGreeter() {
return function() { console.log('Hello!'); };
}
这种灵活性让我们可以玩出各种花样,比如立即执行函数(IIFE):
(function() {
console.log('我立即执行啦!');
})();
三、柯里化:七龙珠的召唤术
现在进入正题!柯里化的概念其实很简单:把接受多个参数的函数变成一系列接受单一参数的函数。
想象一下《七龙珠》中的场景:要召唤神龙,需要集齐7颗龙珠。柯里化函数就像是在逐步收集龙珠(参数),当集齐所有龙珠(参数)时,就能召唤神龙(执行函数)!
1. 普通函数 vs 柯里化函数
// 普通加法函数
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// 柯里化后的加法函数
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6
2. 自动柯里化的神奇函数
手动写柯里化函数太麻烦了,我们来写一个通用的柯里化函数:
function curry(fn) {
// 返回一个函数
// ...args 收集所有参数(自由变量)
let judge = (...args) => {
// 如果参数数量足够,就执行原函数
if (args.length === fn.length) {
return fn(...args);
}
// 否则返回一个新函数继续收集参数
return (...newArgs) => judge(...args, ...newArgs);
};
return judge;
}
使用这个魔法函数:
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
3. 柯里化的实际应用
场景1:参数复用
// 普通函数
function log(level, time, message) {
console.log(`${level} ${time}: ${message}`);
}
// 柯里化后
const curriedLog = curry(log);
const logError = curriedLog('ERROR');
const logErrorToday = logError('2023-06-15');
logErrorToday('系统崩溃了!'); // ERROR 2023-06-15: 系统崩溃了!
场景2:延迟执行
const fetchData = curry(function(url, params, callback) {
// 模拟API请求
setTimeout(() => {
callback(`从${url}获取数据: ${JSON.stringify(params)}`);
}, 1000);
});
const getUser = fetchData('/api/user');
const getUserWithLimit = getUser({ limit: 10 });
getUserWithLimit(data => console.log(data));
// 1秒后输出: 从/api/user获取数据: {"limit":10}
四、柯里化的高级技巧
1. 无限参数柯里化
前面的柯里化函数需要知道参数数量,我们可以改进它:
function infiniteCurry(fn) {
return function curried(...args) {
// 如果没有参数传入,就执行函数
if (args.length === 0) {
return fn.apply(this, []);
}
return function(...newArgs) {
if (newArgs.length === 0) {
return fn.apply(this, args);
}
return curried.apply(this, args.concat(newArgs));
};
};
}
const sum = infiniteCurry((...nums) => nums.reduce((a, b) => a + b, 0));
console.log(sum(1)(2)(3)()); // 6
console.log(sum(1, 2)(3, 4)()); // 10
2. 反柯里化
有时候我们也需要把柯里化函数变回普通函数:
Function.prototype.uncurry = function() {
return (...args) => {
let fn = this;
for (let arg of args) {
fn = fn(arg);
}
return fn;
};
};
const uncurriedAdd = curriedAdd.uncurry();
console.log(uncurriedAdd(1, 2, 3)); // 6
五、柯里化的性能考量
柯里化虽然强大,但也有一些需要注意的地方:
- 内存占用:每个柯里化层级都会创建一个新函数,可能增加内存使用
- 调用栈:深层嵌套的柯里化可能导致调用栈变深
- 可读性:过度使用柯里化可能降低代码可读性
建议在以下场景使用柯里化:
- 需要部分应用参数时
- 需要创建函数变体时
- 函数组合和管道操作时
六、柯里化的趣味应用
1. 函数组合
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);
console.log(shout('hello')); // HELLO!
2. 验证器构建器
const createValidator = curry((predicate, errorMsg, value) => {
return predicate(value) ? [] : [errorMsg];
});
const isRequired = createValidator(x => x !== '', '不能为空');
const isEmail = createValidator(
x => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(x),
'必须是有效的邮箱'
);
console.log(isRequired('')); // ["不能为空"]
console.log(isEmail('test@example.com')); // []
八、总结:柯里化的力量
柯里化就像是编程世界的乐高积木,它让我们能够:
- 分解复杂问题:将多参数函数拆解为单参数函数链
- 提高代码复用:通过部分应用创建新函数
- 增强函数组合能力:便于创建函数管道
- 延迟执行:只在需要时才真正执行
记住我们的七龙珠比喻:每个柯里化步骤就像收集一颗龙珠,当集齐所有龙珠(参数)时,就能召唤神龙(执行函数)!
希望这篇笔记能帮助你理解柯里化这个强大的概念。下次当你看到curry这个词时,不仅能想到美味的咖喱,还能想到这个强大的函数式编程技术!
Happy Coding! 🚀