在计算机科学中 ,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
柯里化_百度百科
如果读者还不了解柯里化,推荐阅读(写多参数函数总重复传值?用柯里化3步搞定 - 掘金)
一、为什么要做柯里化
柯里化是函数式编程中优化参数传递的重要技巧,核心价值在于提升函数的灵活性与复用性。
它能将多参数函数拆解为多次传参的过程,实现参数复用------ 固定通用参数(如接口基础地址),后续调用只需传递变化部分,减少重复代码。
支持延迟执行 ,分步收集参数(如表单分步骤填写后校验),最后统一执行。结合占位符还能灵活调整传参顺序,应对复杂场景。
本文将带读者从通用柯里化函数 =>支持占位符 =>支持剩余参数理解柯里化函数
二、柯里化函数的实现
2.1 通用柯里化函数
通用柯里化函数的实现的思路是收集传入参数 的长度,当收集参数长度 大于等于(>= )原函数的参数长度时,调用原函数。
js
function curry(fn, ...args) {//传入一个函数,并收集参数
const requiredArgs = fn.length;//函数的参数长度
return function(...newArgs) {//返回一个函数体,函数体收集参数
//注意:这里形成了一个闭包保存上次收集的参数数组:args:Array[]
const allArgs = [...args, ...newArgs]; //解构两个数组并合并 形成新数组
if (allArgs.length >= requiredArgs) {//判断收集参数是否足够
return fn.apply(this, allArgs);// 足够执行原函数
} else {
return curry.call(this, fn, ...allArgs);//否则再次调用柯里化函数 返回一个新函数
}
};
}
测试代码以及结果:
js
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6
// 参数复用示例
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = curry(greet, 'Hello');
console.log(sayHello('Alice')); // "Hello, Alice!"
console.log(sayHello('Bob')); // "Hello, Bob!"
// 处理对象参数的函数
function getUserInfo(name, age, address) {
return `Name: ${name}, Age: ${age}, Address: ${address}`;
}
const getBeijingUser = curry(getUserInfo, undefined, undefined, 'Beijing');
console.log(getBeijingUser('张三', 30)); // "Name: 张三, Age: 30, Address: Beijing"
优点:代码简单,易于理解,代码薄弱的同学更好理解。
缺点:
- 对特殊函数参数支持不足
- 不支持参数灵活传递
- this 指向可能不符合预期
- 不支持动态参数长度场景
2.2 支持占位符的柯里化函数
支持占位符的柯里化函数,核心思想是维护一个参数数组的前 fn.length 个参数 ,还未填入的参数,调用时将会用undefined 暂时替代,填入新参数更新参数数据,当前 fn.length 的参数中不存在 undefined 时调用原函数
js
function curry(fn, ...args) {
const requiredArgs = fn.length;
// 辅助函数:检查是否所有参数都已填充(无占位符)
const isComplete = (args) => {
return args.length >= requiredArgs &&
args.slice(0, requiredArgs).every(arg => arg !== undefined);
//Array.every 当所有返回值都为true 返回ture
};
// 辅助函数:合并新旧参数,替换占位符
const mergeArgs = (oldArgs, newArgs) => {
const merged = [...oldArgs];
let newArgIndex = 0;//遍历新参数的索引
// 先替换已有占位符
for (let i = 0; i < merged.length && newArgIndex < newArgs.length; i++) {
//遍历现有的参数,将参数中的undefined优先替代
// 举例:merged: [undefined,undefined,1] new:["manba",undefined,"Heaven"]
//=>merged: ["manba",undefined,1]
if (merged[i] === undefined) {
merged[i] = newArgs[newArgIndex++];//注意:第二次传入的参数也可以为undefined
}
}
// 补充剩余的新参数
while (newArgIndex < newArgs.length) {
merged.push(newArgs[newArgIndex++]);
}
return merged; //merged:["manba",undefined,1,"Heaven"]
};
// 如果参数已完整(无占位符),执行原函数
if (isComplete(args)) {
return fn.apply(this, args.slice(0, requiredArgs));
}
// 否则返回新函数继续收集参数
return function (...newArgs) {
const mergedArgs = mergeArgs(args, newArgs);//调用函数 mergeArgs
return curry.call(this, fn, ...mergedArgs);
};
}
测试代码笔者选用了上一篇文章中的使用场景 的例子,结合上一篇的理解,可以帮助读者更好的理解柯里化,测试代码及其结果:
js
// 应用场景:多参数费用计算
function calculateTotal(
productCost, // 商品费
packagingFee, // 打包费
shippingFee, // 运费
tax, // 纳税
tariff, // 关税
redEnvelope, // 红包
coupon // 代金券
) {
return productCost + packagingFee + shippingFee + tax + tariff - redEnvelope - coupon;
}
// 1. 柯里化费用计算函数
const curriedCalculate = curry(calculateTotal);
// 2. 第一步:用占位符固定后5个参数(前2个参数留空)
const fixedCommonCosts = curriedCalculate(undefined, undefined, 5, 0, 0, 0, 0);
// 此时参数为:[_, _, 5, 0, 0, 0, 0](含占位符,不执行原函数)
// 3. 第二步:传入前2个参数(替换占位符)
const boxedMeal = fixedCommonCosts(35, 1);
console.log(`盒装餐品总价:${boxedMeal}元`); // 35+1+5=41元
const baggedGoods = fixedCommonCosts(20, 0);
console.log(`袋装商品总价:${baggedGoods}元`); // 20+0+5=25元
// 4. 支持部分替换和多轮替换
const partialReplace = curriedCalculate(undefined, 1, 5, undefined, 0, 0, 0); // 部分占位
const withTax = partialReplace(35, 2); // 替换剩余占位符
console.log(`含税费总价:${withTax}元`); // 35+1+5+2=43元
// 5. 支持超额参数(自动忽略多余部分)
const extraArgs = fixedCommonCosts(100, 1, '这是多余的参数');
console.log(`超额参数处理:${extraArgs}元`); // 100+1+5=106元
优点:
- 支持参数灵活传递
- 支持非顺序传参
缺点:
- 特殊参数处理缺陷:比如原函数采用"..."收集参数
- 可以用 "_" 替代 undefined 更为直观
- this 指向问题 :虽然通过
call(this)
和apply(this)
传递上下文,但如果原函数是箭头函数(无this
)或柯里化后的函数被动态改变this
(如通过bind
),可能仍存在语义不一致。
2.3 支持剩余参数(收集符...)的柯里化函数
支持剩余参数( ... )的核心思想就是,先判断函数 fn 是否中是否存在 "..." 符号,存在该符号则允许直接传空参数调用函数 ,或者使用提供的 "run" 关键词运行 原函数,否则继续收集参数返回函数体
需要一些前端基础才能更好的理解
js
const _ = Symbol('placeholder');
function curry(fn, ...args) {
// 检测函数类型:是否含剩余参数(...args)
const hasRestParams = () => {
const fnStr = fn.toString().replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*/g, '');
//移除注释代码的 正则运算 因为注释代码中可能有"..."
return fnStr.includes('...');
};
// 工具函数:过滤占位符,获取有效参数
const getValidArgs = (argList) => argList.filter(arg => arg !== _);
// 工具函数:合并新旧参数,替换占位符
const mergeArgs = (oldArgs, newArgs) => {
const merged = [...oldArgs];
let newIdx = 0;
// 先替换旧参数中的占位符
for (let i = 0; i < merged.length && newIdx < newArgs.length; i++) {
if (merged[i] === _) merged[i] = newArgs[newIdx++];
}
// 补充剩余新参数
while (newIdx < newArgs.length) merged.push(newArgs[newIdx++]);
return merged;
};
// 核心:判断是否执行原函数
const isRestFn = hasRestParams();
const requiredArgs = fn.length;
const validArgs = getValidArgs(args);
// - 普通函数:有效参数数 >= 形参长度 → 执行
// - 剩余参数函数:两种触发方式:显式传空参() || 主动调用.run()方法
const shouldExecute = !isRestFn && validArgs.length >= requiredArgs;
// 执行原函数的统一方法(供内部判断和外部主动调用)
const execute = () => fn.apply(this, validArgs);
// 普通函数满足条件直接执行;剩余参数函数返回带.run()的柯里化函数
if (shouldExecute) return execute();
// 返回柯里化函数(带.run()方法,支持主动触发执行)
const curriedFn = function (...newArgs) {
// 若传空参,且是剩余参数函数 => 执行原函数
if (isRestFn && newArgs.length === 0) return execute();
// 否则继续合并参数
const mergedArgs = mergeArgs(args, newArgs);
return curry.call(this, fn, ...mergedArgs);
};
// 给剩余参数函数的柯里化结果加.run()方法(主动执行入口)
if (isRestFn) curriedFn.run = execute;
return curriedFn;
}
这里笔者就用简单的加减法来测试该代码
js
const sumRest = (start, ...args) => args.reduce((a, b) => a + b, 0) - start;
const curriedSumRest = curry(sumRest);
// 方式1:多次传参 + 空参触发执行
const result1 = curriedSumRest(1)(1)(2)(3)();
console.log(result1); // 1+2+3-1=5
// 方式2:多次传参 + .run()主动执行
const result2 = curriedSumRest(_, 2)(1)(5).run();
console.log(result2); // 2+5-1=6
优点:
- 支持灵活的传参
- 支持(...)收集符,能支持更多种类的函数
- 用 " _ " 替代 "undefined" 更加直观
- 可以用 ".run()" 运行函数减少阅读负担
- 支持非顺序传参
缺点:
- 闭包会带来大量性能开销
- this 指向问题依旧存在
三、总结
本文通过三个版本的柯里化实现(基础版 、支持占位符版 、支持剩余参数版),展现了从简单到复杂的功能演进,核心是逐步收集参数并在满足条件时执行原函数。
笔者还想写使用场景,但大脑使用过度X_X