一次搞懂柯里化:从最简单代码到支持任意函数,这篇让你不再踩参数传递的坑

在计算机科学中柯里化(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

相关推荐
前端大卫几秒前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘16 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare17 分钟前
浅浅看一下设计模式
前端
Lee川21 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端