JavaScript函数柯里化
目录
函数柯里化
一、柯里化的核心概念
1. 基本定义
2. 核心特点
二、柯里化的实现方式
1. 手动实现
2. ES6 箭头函数简化
三、柯里化的应用场景
1. 参数复用
2. 延迟执行
3. 函数组合
四、柯里化与部分应用(Partial Application)的区别
五、柯里化的优缺点
优点:
缺点:
六、实际应用案例
1. React 高阶组件(HOC)
2. Redux 中间件
七、总结
_.curry 详解
一、核心功能
二、源码实现分析
三、关键逻辑拆解
1. 参数合并与占位符处理
2. 参数数量检查与执行
3. 闭包与状态保存
四、边界处理与设计亮点
1. 动态 arity 支持
2. 占位符灵活性
3. 链式调用与参数累积
五、使用示例
1. 基础柯里化
2. 自定义 arity
六、总结
函数柯里化
柯里化(Currying) 是一种将接受 多个参数的函数 转换为一系列 接受单个参数的函数 的技术,其核心思想是 分步传递参数,逐步生成更具体的函数。这种技术由数学家 Haskell Curry 提出,广泛应用于函数式编程中,以提高代码的复用性和组合性。
一、柯里化的核心概念
1. 基本定义
-
普通函数:一次传递所有参数。
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. 核心特点
-
参数复用:固定部分参数,生成更专用的函数。
-
延迟执行:参数未全部传递时,函数不会执行。
-
函数组合:便于组合多个函数形成新功能。
二、柯里化的实现方式
1. 手动实现
通过闭包和递归逐步收集参数:
function curry(fn) {
return function curried(...args) {
// 参数足够时执行原函数,否则返回新函数继续收集参数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 使用示例
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
2. ES6 箭头函数简化
利用箭头函数隐式返回特性:
const curry = (fn) =>
curried = (...args) =>
args.length >= fn.length
? fn(...args)
: (...nextArgs) => curried(...args, ...nextArgs);
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
三、柯里化的应用场景
1. 参数复用
固定部分参数,生成特定功能的函数:
// 通用日志函数
const log = (level, message) => console.log(`[${level}] ${message}`);
// 柯里化生成特定级别的日志函数
const debugLog = curry(log)('DEBUG');
debugLog('User logged in'); // [DEBUG] User logged in
const errorLog = curry(log)('ERROR');
errorLog('Database connection failed'); // [ERROR] Database connection failed
2. 延迟执行
动态组合参数,最后统一执行:
// 发送 API 请求
const sendRequest = (method, url, data) => { /* ... */ };
// 柯里化生成 POST 请求函数
const post = curry(sendRequest)('POST');
post('/api/users')({ name: 'Alice' });
// 进一步生成特定路径的 POST 函数
const postToUsers = post('/api/users');
postToUsers({ name: 'Bob' });
3. 函数组合
结合 compose
或 pipe
实现复杂逻辑:
// 组合多个函数
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
// 柯里化处理数据流
const toUpperCase = str => str.toUpperCase();
const exclaim = str => str + '!';
const emphasize = compose(curry(exclaim), curry(toUpperCase));
console.log(emphasize('hello')); // "HELLO!"
四、柯里化与部分应用(Partial Application)的区别
特性 | 柯里化(Currying) | 部分应用(Partial Application) |
---|---|---|
参数传递 | 每次只接受一个参数,逐层嵌套函数 | 可一次传递多个参数,固定部分参数 |
返回结果 | 返回链式函数,直到所有参数收集完毕 | 直接返回接受剩余参数的函数 |
灵活性 | 严格按参数顺序分解 | 可任意选择固定参数的位置(如 _ 占位符) |
示例对比:
// 柯里化
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6
// 部分应用
const partialAdd = (a, b) => c => a + b + c;
partialAdd(1, 2)(3); // 6
五、柯里化的优缺点
优点:
-
提高函数复用性:通过参数复用生成专用函数。
-
增强代码可读性:链式调用更符合自然语言逻辑(如
sendRequest('GET')('/api/data')
)。 -
便于函数组合:与高阶函数(
map
、filter
)结合更灵活。
缺点:
-
性能开销:多次嵌套函数调用可能影响性能。
-
代码复杂度:过度使用会降低代码可读性(如深层嵌套的箭头函数)。
-
参数顺序限制:必须按定义顺序传递参数。
六、实际应用案例
1. React 高阶组件(HOC)
通过柯里化传递配置参数:
const withLoading = (Component) => ({ isLoading, ...props }) =>
isLoading ? <Spinner /> : <Component {...props} />;
// 柯里化增强组件
const EnhancedTable = withLoading(Table);
<EnhancedTable isLoading={true} data={data} />
2. Redux 中间件
处理异步 Action 的柯里化中间件:
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
七、总结
-
柯里化本质:将多参数函数转换为单参数函数的链式调用。
-
核心价值:参数复用、延迟执行、增强函数组合能力。
-
适用场景:高频复用参数、动态生成函数、复杂逻辑拆解。
-
注意事项:避免过度使用,权衡可读性与灵活性。
代码实践建议:
-
使用 Lodash 的
_.curry
或 Ramda 的R.curry
简化柯里化过程。 -
结合 TypeScript 增强类型提示,避免参数类型错误。
_.curry
详解
一、核心功能
_.curry
将多参数函数转换为柯里化(Currying)函数,允许 分步传递参数,直到参数数量达到指定 arity
(默认取原函数的形参数量 func.length
)。
支持特性:
-
占位符(
_
):允许跳过某些参数位置,后续调用填充。 -
灵活调用:支持单参数调用(如
curried(a)(b)(c)
)或批量传递(如curried(a, b, c)
)。
二、源码实现分析
以下为简化后的核心逻辑(基于 Lodash v4.17.21):
import placeholder from './placeholder';
function curry(func, arity = func.length) {
// 生成柯里化包装函数
function curried(...args) {
// 合并当前参数与之前的参数(处理占位符)
const combined = combineArguments(args, curried.placeholder, curried.args);
// 检查是否满足参数数量要求
if (combined.length >= arity) {
return executeFunc(func, this, combined.slice(0, arity));
} else {
// 继续返回柯里化函数,保存已合并的参数
return createCurried(func, arity, combined);
}
}
// 初始化参数存储和占位符配置
curried.args = [];
curried.placeholder = placeholder;
return curried;
}
// 辅助函数:合并新旧参数,替换占位符
function combineArguments(newArgs, placeholder, oldArgs = []) {
const result = [...oldArgs];
let newIndex = 0;
// 遍历旧参数,替换占位符
for (let i = 0; i < result.length; i++) {
if (result[i] === placeholder && newIndex < newArgs.length) {
result[i] = newArgs[newIndex++];
}
}
// 添加剩余新参数到末尾
while (newIndex < newArgs.length) {
result.push(newArgs[newIndex++]);
}
return result;
}
// 辅助函数:创建新的柯里化函数,继承参数和配置
function createCurried(func, arity, args) {
const newCurried = curry(func, arity);
newCurried.args = args;
return newCurried;
}
// 辅助函数:执行原函数,绑定 this 和参数
function executeFunc(func, thisArg, args) {
return func.apply(thisArg, args);
}
三、关键逻辑拆解
1. 参数合并与占位符处理
-
combineArguments
函数:-
输入:新参数
newArgs
、占位符标识placeholder
、旧参数oldArgs
。 -
步骤:
-
遍历旧参数,用新参数替换占位符(从左到右填充)。
-
将剩余新参数追加到结果末尾。
-
-
示例:
// 旧参数: [1, _, 3] // 新参数: [2] // 合并结果: [1, 2, 3]
-
2. 参数数量检查与执行
-
条件判断:
-
当已合并的参数数量
>= arity
时,调用原函数func
。 -
否则,生成新的柯里化函数(携带已合并的参数)。
-
3. 闭包与状态保存
-
curried
函数:-
通过闭包保存
arity
和原函数func
。 -
通过属性
curried.args
存储已合并的参数。 -
通过
curried.placeholder
标识占位符(如_
)。
-
四、边界处理与设计亮点
1. 动态 arity
支持
-
默认值:
arity = func.length
,但手动指定可避免func.length
不准确的问题(如参数默认值、剩余参数)。// 原函数参数含默认值,func.length 可能不符合预期 function sum(a, b = 2) {} console.log(sum.length); // 1 // 手动指定 arity=2 const curriedSum = _.curry(sum, 2);
2. 占位符灵活性
-
跳过参数位置:允许后续调用填充任意位置的占位符。
const curried = _.curry((a, b, c) => a + b + c); curried(1, _, 3)(2); // 6(等效于 curried(1, 2, 3))
3. 链式调用与参数累积
-
多次调用:每次调用将参数合并到已有参数列表中,直到参数足够。
const curriedAdd = _.curry((a, b, c) => a + b + c); const temp = curriedAdd(1); // 参数: [1] const temp2 = temp(2); // 参数: [1, 2] temp2(3); // 6
五、使用示例
1. 基础柯里化
const add = (a, b, c) => a + b + c;
const curriedAdd = _.curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1, _, 3)(2); // 6(使用占位符)
2. 自定义 arity
function dynamicArgs(...args) {
return args.slice(0, 3).join('-');
}
const curried = _.curry(dynamicArgs, 3); // 手动指定 arity=3
curried('a')('b')('c'); // 'a-b-c'
六、总结
设计要点 | 实现细节 |
---|---|
参数合并 | 替换占位符,累积参数直到满足 arity |
状态管理 | 通过闭包和函数属性保存已收集参数和配置 |
占位符支持 | 动态替换机制,允许灵活填充参数位置 |
动态 arity | 支持手动指定参数数量,避免依赖 func.length 的局限性 |
链式调用 | 返回新柯里化函数,直至参数足够后执行原函数 |
源码亮点:
-
模块化辅助函数:
combineArguments
、createCurried
等提升可维护性。 -
占位符替换策略:确保参数填充顺序的灵活性。
-
闭包状态管理:优雅地保存中间参数,避免污染外部作用域。