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

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

相关推荐
渣哥1 天前
还在写繁琐监听器?Spring @EventListener 注解让你代码瞬间简化
javascript·后端·面试
Keepreal4961 天前
实现一个简单的hello world vs-code插件
前端·javascript·visual studio code
用户458203153171 天前
CSS 层叠层 (@layer) 详解:控制样式优先级新方式
前端·css
月弦笙音1 天前
【Vue组件】封装组件该考虑的核心点
前端·javascript·vue.js
清风细雨_林木木1 天前
HttpOnly 是怎么防止 XSS 攻击的?
前端·xss
用户2519162427111 天前
Node之单表基本查询
前端·javascript·node.js
文心快码BaiduComate1 天前
基于YOLOv8的动漫人脸角色识别系统:Comate完成前端开发
前端·后端·前端框架
需要兼职养活自己1 天前
react 之redux
前端·react.js·redux
ら陈佚晨1 天前
React 18 的核心设计理念:并发渲染
前端·javascript·react.js·前端框架·fiber