js面试必考题之柯里化函数 一文助你拿下js考题

8 阅读7分钟

柯里化(Currying)是一个既基础又高级的概念。它不仅是函数式编程的核心技巧之一,更是大厂面试中高频出现的经典考题。搞懂柯里化,从它的定义、原理到手写实现,再到实际应用场景,轻松解决这一考题。

什么是柯里化?

简单来说,柯里化是一种将接受多个参数的函数转换成一系列只接受单个参数的函数的技术。听起来有点绕?我们来看一个最直观的例子。

假设我们有一个普通的加法函数:


function add(a, b) {
  return a + b;
}
console.log(add(1, 2)); // 输出: 3

这是一个再普通不过的函数,它一次性接收两个参数 ab 并返回它们的和。

而经过柯里化处理后,这个函数就变成了这样:


function add(a) {
  return function(b) {
    return a + b;
  };
}
// 或者用箭头函数更简洁: const add = a => b => a + b;
console.log(add(1)(2)); // 输出: 3

现在,add 函数不再一次性接收所有参数,而是先接收第一个参数 a,然后返回一个新的函数。这个新函数再接收第二个参数 b,最终完成计算并返回结果。

这就是柯里化的核心思想:将一个多参数函数的调用,拆分成多次单参数函数的调用

特性普通函数 (Normal Function)柯里化函数 (Curried Function)
参数接收方式一次性接收所有必需参数。分多次调用,每次接收一个或多个参数。
调用形式fn(a, b, c)fn(a)(b)(c)fn(a, b)(c)
核心目的完成特定任务。参数复用、创建专用函数、延迟执行。
代码示例function add(a, b) { return a + b; } add(1, 2); // 3const add = a => b => a + b; const addOne = add(1); addOne(2); // 3
灵活性较低,每次调用都需要提供完整参数。极高,可以固定部分参数,生成新的、语义更明确的函数。
适用场景通用、一次性的计算逻辑。需要配置、组合或复用部分逻辑的场景(如日志、API请求封装等)。

为什么要用柯里化?它的价值何在?

你可能会问,把一个简单的 add(1, 2) 变成 add(1)(2) 有什么意义?这不是让代码变得更复杂了吗?

其实不然,柯里化的真正威力在于 参数复用创建高度可配置的函数。它能让我们写出更具表现力和可维护性的代码。

1. 固定部分参数,创建专用函数

这是柯里化最实用的场景之一。想象一下,我们需要在项目中打印不同类型的日志(如 info、error、warning)。我们可以先定义一个通用的日志函数,然后通过柯里化来固定日志类型,从而生成语义明确的专用函数。

// 来自 4.js
// 通用日志函数
const log = type => message => {
  console.log(`[${type}]: ${message}`);
};

// 通过柯里化固定 'type' 参数,创建专用日志函数
const errorLog = log("error");
const infoLog = log("info");

// 使用起来非常清晰
errorLog("接口异常"); // 输出: [error]: 接口异常
infoLog("页面加载完成"); // 输出: [info]: 页面加载完成

在这个例子中,log 是一个柯里化后的函数。我们通过 log("error") 调用,固定了 type 为 "error",返回了一个新的函数 errorLog。这个新函数只需要关心 message 这一个参数即可。这不仅减少了重复代码,还极大地提高了代码的可读性和语义化程度。

2. 延迟执行与组合

柯里化函数在接收到足够参数之前不会执行,这种延迟执行的特性使其非常适合用于函数组合(Function Composition),构建复杂的逻辑管道。

手写一个通用的柯里化函数

面试官通常不会满足于你只会写 a => b => a + b 这种形式。他们更希望看到你能否实现一个通用的 curry 函数,它可以将任意多参数的函数进行柯里化。

我们知道实现柯里化的关键在于 闭包递归

  • 闭包:用于保存已经收集到的参数。
  • 递归:用于持续返回新函数以接收后续参数。
  • 退出条件:当收集到的参数数量等于原函数所需的参数数量(fn.length)时,执行原函数。

下面,我们来看一个经典的实:


function add(a, b, c, d) {
  return a + b + c + d;
}

// 通用柯里化函数
function curry(fn) {
  // 返回一个名为 curried 的内部函数
  return function curried(...args) {
    // 判断已收集的参数数量是否 >= 原函数所需参数数量
    if (args.length >= fn.length) {
      // 如果是,则执行原函数
      return fn(...args);
    }
    // 如果不是,则返回一个新函数,继续收集剩余参数
    // 这里利用了闭包,curried 函数可以访问外部的 args
    return (...rest) => curried(...args, ...rest);
  };
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)(4)); // 输出: 10
// 它甚至支持不那么严格的调用方式
console.log(curriedAdd(1, 2)(3, 4)); // 同样输出: 10

让我们拆解一下这个 curry 函数的工作原理:

  1. curry(fn) :接收一个待柯里化的函数 fn
  2. return function curried(...args) :返回一个新函数 curried,它使用剩余参数 ...args 来收集首次调用时传入的所有参数。
  3. if (args.length >= fn.length) :检查当前收集到的参数数量 args.length 是否已经满足原函数 fn 的要求(fn.length 是函数的形参个数)。如果满足,就立即执行 fn(...args) 并返回结果。
  4. return (...rest) => curried(...args, ...rest) :如果不满足,则返回一个新的箭头函数。这个新函数会接收下一批参数 ...rest,并将它们与之前收集的 ...args 合并,然后递归地调用 curried。这里巧妙地利用了闭包,使得每次返回的新函数都能“记住”之前所有的参数。

这个实现非常优雅,它不仅支持严格的逐个传参(curriedAdd(1)(2)(3)(4)),也支持一次传入多个参数(curriedAdd(1, 2)(3, 4)),具有很好的灵活性。

要点说明关联代码/概念
1. 闭包 (Closure)这是柯里化的基石。内部返回的函数必须能“记住”并访问外部函数已经接收到的参数。所有柯里化实现都依赖闭包来保存状态。
2. 递归/循环 (Recursion/Looping)用于持续返回新函数以收集后续参数,直到满足执行条件。curried 函数通过递归调用自身来收集参数。
3. 退出条件必须有一个明确的条件来判断何时停止返回新函数并执行原函数。通常是比较已收集参数数量与原函数所需参数数量 (fn.length)。if (args.length >= fn.length) { return fn(...args); }
4. 参数收集需要一种机制来累积每次调用传入的参数。通常使用剩余参数 (...args) 和展开运算符 (...rest)。function curried(...args)(...rest) => curried(...args, ...rest)
5. 通用性面试官期望看到的不是一个只能处理特定函数(如 add)的柯里化,而是一个能处理任意 n 元函数的通用 curry 高阶函数。例子中的 curry 函数就是一个通用实现。
6. 应用场景理解能否结合实际例子阐述柯里化的价值,是区分死记硬背和真正理解的关键。const errorLog = log("error"); 展示了参数复用和创建专用函数。

总结

柯里化远不止是一个炫技的面试题。它背后体现的是函数式编程中“函数是数据”的核心思想。通过将函数拆解、组合和复用,我们可以构建出更加模块化、可测试和富有表达力的代码。

  • 核心原理:闭包(保存状态)+ 递归(持续收集)。
  • 主要用途:参数复用、创建专用函数、延迟执行、函数组合。
  • 面试要点:不仅要理解概念,更要能手写一个通用的 curry 实现,并解释其工作原理。

下次当面试官问起柯里化时,你可以自信地从 add(1)(2) 讲到通用 curry 函数的实现,再结合 log 函数的例子谈谈工程实践,然后拿下面试。