一个小小的柯里化函数

看段简单的代码:

JS 复制代码
function curry(fn) {
  // curry 函数接收一个原始函数 fn 作为参数
  return function curried(...args) {
    // curried 是被 curry 函数返回的"柯里化"后的函数。
    // 每次调用 curried 时,它都会收集当前传入的参数 args。

    // 1. 判断是否收集到足够的参数
    // fn.length 是原始函数 fn 期望的参数个数(即函数的"元数"或"arity")。
    // args.length 是当前已经收集到的参数个数。
    if (args.length >= fn.length) {
      // 如果当前收集到的参数数量 (args.length) 大于或等于
      // 原始函数 fn 期望的参数数量 (fn.length),
      // 说明我们已经收集到了足够的参数来执行原始函数 fn。

      // 使用 apply 方法调用原始函数 fn。
      // apply 的第一个参数是 this 的上下文,这里我们保持调用时的 this。
      // 第二个参数是一个数组,包含了所有收集到的参数 args。
      return fn.apply(this, args);
    } else {
      // 2. 参数不足,返回一个新的函数继续收集参数
      // 如果当前收集到的参数数量不足以执行原始函数 fn,
      // 那么 curried 函数会返回一个新的函数。
      return function(...args2) {
        // 这个新函数接收额外的参数 args2。
        // 它会递归地调用 curried 函数本身。

        // 使用 args.concat(args2) 将之前收集的参数 (args)
        // 和当前新传入的参数 (args2) 合并成一个新的参数数组。
        // 然后,将这个合并后的参数数组传递给 curried 函数进行下一次判断。
        // 同样,使用 apply 保持 this 上下文。
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}

// 使用示例
function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

逐步解析 curriedSum 的调用过程:

假设 sum 函数的 fn.length 是 3。

  1. const curriedSum = curry(sum);

    • curry(sum) 被调用。
    • 它返回一个名为 curried 的内部函数。curriedSum 现在就是这个 curried 函数。
    • 此时,curried 函数内部的闭包记住了 fn 就是 sum。
  2. curriedSum(1)

    • curried 函数被调用,args 是 ``。
    • args.length (1) < fn.length (3),条件 if (args.length >= fn.length) 不满足。
    • 进入 else 分支。
    • 返回一个新的匿名函数。这个匿名函数内部的闭包记住了当前的 args 是 ``。
    • 所以 curriedSum(1) 的结果是一个新的函数。
  3. curriedSum(1)(2)

    • 上一步返回的匿名函数被调用,args2 是 ``。
    • 这个匿名函数执行 curried.apply(this, args.concat(args2))。
    • args (来自上一个闭包) 是 ,`args2` 是 。
    • args.concat(args2) 结果是 ``。
    • 现在 curried 函数被递归调用,传入的 args 是 ``。
    • 在这次 curried 调用中,args.length (2) < fn.length (3),条件不满足。
    • 再次进入 else 分支。
    • 返回一个新的匿名函数。这个匿名函数内部的闭包记住了当前的 args 是 ``。
    • 所以 curriedSum(1)(2) 的结果又是一个新的函数。
  4. curriedSum(1)(2)(3)

    • 上一步返回的匿名函数被调用,args2 是 ``。
    • 这个匿名函数执行 curried.apply(this, args.concat(args2))。
    • args (来自上一个闭包) 是 ,`args2` 是 。
    • args.concat(args2) 结果是 ``。
    • 现在 curried 函数被递归调用,传入的 args 是 ``。
    • 在这次 curried 调用中,args.length (3) >= fn.length (3),条件满足!
    • 进入 if 分支。
    • 执行 fn.apply(this, args),即 sum.apply(this,)。
    • sum(1, 2, 3) 返回 6。
    • 所以 curriedSum(1)(2)(3) 的结果是 6。

这种柯里化函数的特点和优点:

  • 灵活性 :它不仅支持链式调用(每次一个参数),也支持一次性提供所有参数,或者分批提供参数,例如 curriedSum(1, 2, 3) 或 curriedSum(1, 2)(3)。

  • 参数复用(Partial Application) :你可以创建一个预先填充了部分参数的新函数。例如:

    js 复制代码
    const addOneAndTwo = curriedSum(1, 2); // addOneAndTwo 现在是一个等待第三个参数的函数
    console.log(addOneAndTwo(3)); // 输出 6 (1 + 2 + 3)
    console.log(addOneAndTwo(10)); // 输出 13 (1 + 2 + 10)
  • 延迟执行 :原始函数 fn 不会立即执行,直到所有必需的参数都已提供。

  • 函数组合:柯里化函数在函数式编程中非常有用,因为它使得函数的组合变得更加容易和自然。

涉及的关键概念:

  • 闭包(Closure) :curried 函数以及它内部返回的匿名函数都形成了闭包。它们记住了自己被创建时的环境(例如 fn,以及每次调用时累积的 args),即使外部函数已经执行完毕,这些内部函数仍然可以访问和操作这些变量。
  • fn.length:这是JavaScript函数的一个内置属性,表示函数声明时期望的参数个数。
  • apply() :用于调用函数,并允许你以数组的形式传递参数,同时可以指定 this 的上下文。
  • 递归(Recursion) :curried 函数在 else 分支中递归地调用了自身,这是实现参数累积和链式调用的关键。
  • this 上下文 :代码中使用了 this,这意味着柯里化后的函数在调用时会保留其 this 上下文。在严格模式下,如果函数是作为独立函数调用,this 会是 undefined;如果作为对象方法调用,this 会指向该对象。在非严格模式下,独立函数调用时 this 会指向全局对象(浏览器中的 window)。
相关推荐
溪语流沙11 分钟前
【每天一个CSS | Day19】流光文字与一笔写成的手写签名
前端·javascript·css
逐光者93318 分钟前
STM32——SPI 屏 · Flash · 高速
java·前端·stm32
To_OC26 分钟前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
CDwenhuohuo38 分钟前
electron pc项目打包成桌面端
前端·javascript·electron
前端大斗师41 分钟前
「大于 1000」把 1000 元那单也算进去了:我在 Vue3 订单页对了 10 句话
前端·人工智能·typescript·大模型·原力计划
Jmyd01231 小时前
用 Web VR 引擎搭建 3D 虚拟展厅:从素材上云到多端实时渲染的完整数据链路
前端·3d·vr·三维数字化
刘天远2 小时前
Agent成本核算实现:事件表、状态分布与Python归集
前端·数据库·人工智能·python
欣欣之王来了2 小时前
开发环境准备:Node.js、npm、VS Code安装配置
前端·学习·架构·项目·vue3教程
xiaolinudao1232 小时前
将多个 Excel 表格中的数据合并到单个表中|6 种实现方案全解析
java·前端·excel
LRL_3 小时前
跨平台 Node 模块安装指南:如何利用 pnpm/npm 配置`supportedArchitectures` 锁定平台依赖
前端·npm·node.js