《吃透JavaScript柯里化:闭包+递归底层原理》

0 阅读8分钟

一文吃透JavaScript柯里化函数

在JavaScript函数式编程中,柯里化(Currying)是一个高频出现但容易被混淆的概念。很多开发者只知道它是“把多参数函数拆分成单参数函数”,却不清楚其底层逻辑、实现方式和实际价值。

一、先看现象:从普通函数到柯里化函数

我们先从最基础的加法函数入手,对比普通函数和柯里化函数的调用差异,直观感受柯里化是什么。

1. 普通多参数函数

最常见的加法函数,需要一次性传入所有参数才能执行:

function add(a, b) {
  return a + b;
}
// 一次性传入所有参数
console.log(add(1, 2)); // 输出:3

这种写法的特点是:参数必须一次性传齐,无法分步骤传递。如果场景需要分步传参(比如先确定第一个加数,后续再确定第二个),普通函数就显得很笨拙。

2. 简单柯里化函数(手动实现)

为了解决分步传参的问题,我们可以手动实现一个柯里化版本的加法函数:

function add(a) {
  // 内层函数,接收第二个参数
  return function(b) {
    return a + b;
  }
}
// 分步传递参数:先传a=1,再传b=2
console.log(add(1)(2)); // 输出:3

这就是柯里化的核心表现:将多参数函数拆解成多个单参数(或少量参数)的嵌套函数,参数可以一个一个传递,直到传完所有必要参数后,执行最终逻辑

3. 多参数场景的延伸

如果函数有更多参数(比如4个),手动嵌套函数会很繁琐,但核心逻辑不变。先看一个4参数的普通加法函数:

function add(a, b, c, d) {
  return a + b + c + d;
}
// 打印函数的形参数量(fn.length是关键!)
console.log(add.length); // 输出:4

这里有个关键知识点:fn.length 是JavaScript函数的内置属性,返回函数形参的数量(不包含默认参数、剩余参数)。这个属性将成为我们实现通用柯里化工具的核心判断依据。

二、核心实现:通用柯里化工具函数(附逐行解析)

手动给每个函数写柯里化版本不现实,我们需要一个通用的柯里化工具(就是你提供的代码),能将任意多参数函数转换成柯里化函数。先贴完整代码,再逐行拆解:

// 通用柯里化工具函数,接收原始函数fn作为参数
function curry(fn) {
    // 返回一个用于收集参数的curried函数
    return function curried(...args) {
      // 核心判断:已收集的参数数量 ≥ 原始函数的形参数量
      if (args.length >= fn.length) {
        // 参数够了,执行原始函数,展开参数数组(避免传数组)
        return fn(...args) // 执行完毕,退出递归
      }
      // 参数不够,返回新函数,继续收集剩余参数
      return (...rest) => curried(...args, ...rest);
    }
}

// 测试:将4参数add函数柯里化
const curriedAdd = curry(add);
// 分步传递4个参数,执行结果
console.log(curriedAdd(1)(2)(3)(4)); // 输出:10

逐行解析核心逻辑(重点!)

  1. function curry(fn) { ... }:柯里化工具的入口,接收需要被柯里化的原始函数fn(比如上面的4参数add函数)。
  2. return function curried(...args) { ... }:返回一个名为curried的函数,用于分步收集参数。这里的...args是剩余参数语法,作用是收集每次调用时传入的所有参数(比如第一次调用传1,args就是[1])。
  3. if (args.length >= fn.length) { ... }:柯里化的“退出条件”。判断当前收集到的参数数量(args.length)是否大于等于原始函数需要的参数数量(fn.length),如果够了,就执行原始函数。
  4. return fn(...args):参数够了,调用原始函数。这里的...args是展开运算符,将收集到的参数数组(比如[1,2,3,4])拆成单个参数,传给原始函数(等价于add(1,2,3,4))。
  5. return (...rest) => curried(...args, ...rest):参数不够时,返回一个新的箭头函数,继续收集剩余参数。...rest收集新传入的参数,然后通过curried(...args, ...rest)递归调用curried函数,将“已收集的参数+新参数”合并,继续收集。

三、底层原理:柯里化的核心是「闭包+递归」

很多人学会了柯里化的写法,却不懂其底层逻辑。其实你总结的一句话非常精准:柯里化的本质是闭包,实现依赖递归

1. 闭包:保存参数的“容器”

在通用柯里化工具中,curried函数是一个闭包函数。它能“记住”外层函数(curry)的变量(fn),以及每次调用时收集到的参数(args)——即使外层函数curry执行完毕,curried函数依然能访问到这些变量。

举个例子:当我们调用curriedAdd(1)时,curried函数收集到args=[1],由于参数不够,返回一个新的箭头函数。这个箭头函数通过闭包,记住了当前的args=[1],等待后续传入新的参数。

2. 递归:重复收集参数的“循环”

递归的作用是“重复收集参数”,直到满足退出条件。每次调用返回的箭头函数,本质上都是在递归调用curried函数,不断合并已有的参数和新传入的参数,直到参数数量达标,执行原始函数并退出递归。

curriedAdd(1)(2)(3)(4)的执行流程理解:

  • 第一步:curriedAdd(1) → args=[1],1<4 → 返回新箭头函数(记住args=[1]);
  • 第二步:调用箭头函数传入2 → rest=[2] → 递归调用curried(1,2) → args=[1,2],2<4 → 返回新箭头函数;
  • 第三步:调用箭头函数传入3 → rest=[3] → 递归调用curried(1,2,3) → args=[1,2,3],3<4 → 返回新箭头函数;
  • 第四步:调用箭头函数传入4 → rest=[4] → 递归调用curried(1,2,3,4) → args=[1,2,3,4],4≥4 → 执行add(1,2,3,4) → 返回10。

3. 关键补充:“严格”与“不严格”柯里化

你提到的“严格不严格”,本质是参数传递的灵活性:

  • 严格柯里化:必须一个一个传递参数,每次只能传1个(比如add(1)(2)(3)(4));
  • 不严格柯里化:允许一次性传递多个参数,也允许分多次传递(比如curriedAdd(1,2)(3,4),同样能得到10)。

我们实现的这个通用工具,就是“不严格柯里化”——因为用了剩余参数...args,可以接收任意数量的参数,灵活性更高,也是实际开发中更常用的方式。

四、实战价值:柯里化到底有什么用?

光懂原理不够,更要知道柯里化在实际开发中的应用。结合你提供的日志函数案例,我们看看柯里化的核心价值。

案例:固定函数参数,实现函数复用

我们有一个日志函数,需要接收“日志类型”和“日志信息”两个参数。通过柯里化,我们可以固定“日志类型”,生成专门的日志函数(比如错误日志、信息日志),实现复用:

// 柯里化版本的日志函数
const log = type => message => {
  console.log(`[${type}]: ${message}`)
}

// 固定第一个参数(日志类型),生成专用日志函数
const errorLog = log("error"); // 固定type="error"
const infoLog = log("info");   // 固定type="info"

// 后续调用只需传入日志信息,无需重复传入日志类型
errorLog("接口异常"); // 输出:[error]: 接口异常
infoLog("页面加载完成"); // 输出:[info]: 页面加载完成

柯里化的3个核心应用场景

  1. 参数复用:像上面的日志函数一样,固定某个/某些参数,生成专用函数,减少重复传参(比如固定接口请求的baseURL、固定日志类型)。
  2. 分步传参:场景需要参数分步获取(比如先获取用户ID,再获取用户详情),柯里化可以先收集部分参数,后续补充完整后再执行。
  3. 函数组合:柯里化是函数式编程的基础,配合compose、pipe等函数,可以实现更复杂的函数组合逻辑(比如React中的中间件、状态管理中的逻辑复用)。

五、核心要点总结(必背!)

结合本文案例和你总结的要点,整理出柯里化的核心知识点,面试直接用:

  • 柯里化定义:将多参数函数拆分成多个单参数(或少量参数)的嵌套函数,参数可分步传递,参数齐全后执行函数。
  • 核心本质:闭包+递归——闭包保存已收集的参数,递归重复收集参数直到满足条件。
  • 退出条件:通过fn.length获取原始函数的形参数量,当已收集的参数数量≥fn.length时,执行原始函数。
  • 关键语法:剩余参数(...args)收集参数,展开运算符(...args)传递参数,箭头函数简化嵌套写法。
  • 实际价值:参数复用、分步传参、支持函数组合,提升代码复用性和灵活性。

六、最后:常见误区澄清

  1. 误区1:柯里化就是“嵌套函数”——错!嵌套函数是实现柯里化的方式,柯里化的核心是“分步传参+参数收集”。
  2. 误区2:柯里化只能传1个参数——错!我们实现的是不严格柯里化,支持一次性传多个参数,更灵活。
  3. 误区3:柯里化会提升性能——错!柯里化依赖闭包和递归,会有轻微的性能损耗,其价值在于代码复用和逻辑清晰,而非性能优化。

结尾

柯里化看似抽象,但只要抓住“闭包保存参数、递归收集参数、fn.length判断退出”这三个核心,再结合本文的案例多写几遍,就能彻底掌握。

其实柯里化的本质,是将“多参数的一次性调用”,转化为“参数的分步收集+最终执行”,让函数更灵活、更易复用。