从“炫技”到“利器”:前端面试官眼中的柯里化函数

8 阅读5分钟

从“炫技”到“利器”:前端面试官眼中的柯里化函数

在前端开发的面试江湖中,“柯里化”(Currying)绝对是一个高频出现的“黑话”。很多求职者对它的第一印象往往是“把 f(a, b) 变成 f(a)(b) 的炫技写法”。

然而,在资深面试官眼中,柯里化绝不仅仅是语法糖,它是考察你对闭包、高阶函数、函数式编程思想以及工程化落地能力的绝佳试金石。

今天,我们就剥开柯里化的外衣,从面试必问到实战落地的角度,彻底吃透它。

什么是柯里化:不仅仅是“套娃”

用最通俗的话来说,柯里化就是**“化整为零,延迟满足”**。

它把一个接收多个参数的函数,转换成一系列接收单一参数(或部分参数)的函数链。

面试场景模拟:

面试官:“请实现一个 add 函数,使得 add(1)(2)(3) 能返回 6。”

普通人的写法:

function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 6

柯里化的写法:

function add(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    }
  }
}
add(1)(2)(3); // 6

核心本质:

  • 参数复用: 也就是“偏函数应用”。比如固定了第一个参数 a=1,生成了一个新的函数 addOne,后续只需要传 bc
  • 延迟执行: 函数不会立即求值,而是通过闭包把参数“存”起来,直到参数凑齐了(或者显式触发)才执行。

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

在面试中,只会写死 add(1)(2)(3) 是不够的。面试官通常会追问:“如果参数个数不确定怎么办?如果参数是分批传入的怎么办?”

这时,你需要拿出一个通用的 curry 工具函数。

核心逻辑:

  1. 利用 fn.length 获取原函数需要的参数个数。
  2. 利用闭包保存已传入的参数。
  3. 利用递归判断参数是否凑齐:
    • 如果没凑齐,返回一个新函数继续接参数。
    • 如果凑齐了,执行原函数。

满分代码示例:

function curry(fn) {
  // 1. 获取原函数期望的参数个数
  const arity = fn.length;

  return function curried(...args) {
    // 2. 判断:如果当前收集的参数个数 >= 原函数需要的个数
    if (args.length >= arity) {
      // 参数够了,直接执行原函数
      return fn.apply(this, args);
    } else {
      // 3. 参数不够,返回一个新函数,继续收集剩余参数
      return function(...rest) {
        // 这里利用递归,把之前的参数 args 和新的参数 rest 合并
        return curried.apply(this, [...args, ...rest]);
      };
    }
  };
}

// 测试
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

为什么前端要用柯里化:从“八股文”到“生产力”

这是面试中最能拉开差距的环节。不要只说“为了代码简洁”,要结合具体场景。

参数复用(配置化思想)

这是柯里化最实用的场景。将“易变”的参数和“不变”的参数分离开。

场景: 假设你需要校验手机号、邮箱、身份证。

普通写法:

function check(regExp, string) {
  return regExp.test(string);
}
// 每次都要传正则,代码冗余
check(/^1\d{10}$/, '13800000000');
check(/^1\d{10}$/, '13900000000');

柯里化重构:

const _check = curry((regExp, string) => regExp.test(string));

// 1. 固化正则规则,生成专用工具函数
const isPhone = _check(/^1\d{10}$/);
const isEmail = _check(/^(\w)+@(\w)+$/);

// 2. 业务调用时,语义清晰,无需关心正则细节
isPhone('13800000000'); // true
isEmail('test@example.com'); // true

动态日志系统(环境解耦)

场景: 开发环境和生产环境需要输出不同格式的日志。

const logger = (env) => (level) => (msg) => {
  console.log(`[${env.toUpperCase()}][${level}] ${msg}`);
};

// 预先配置好环境
const prodLogger = logger('prod');
const devLogger = logger('dev');

// 业务中使用
const prodError = prodLogger('ERROR');
prodError('支付接口超时'); // [PROD][ERROR] 支付接口超时

适配函数参数(高阶组件/HOC)

在 React 或 Vue 的生态中,柯里化常用于适配只接收单参数的场景。

场景: Array.prototype.map 的回调函数通常只传一个参数,但我们的工具函数可能需要两个参数。

const multiply = (multiplier, num) => num * multiplier;
const curriedMultiply = curry(multiply);

// 固定倍数为 2,生成一个单参数函数
const double = curriedMultiply(2);

// 完美适配 map
[1, 2, 3].map(double); // [2, 4, 6]

面试避坑指南

在回答柯里化问题时,注意避开以下误区:

  • 误区一:柯里化能提升性能。
    • 纠正: 恰恰相反。柯里化会产生大量的闭包和函数调用栈,性能上其实是有微小损耗的。它的价值在于代码的可维护性、复用性和逻辑解耦,而不是性能。
  • 误区二:必须一个参数一个参数地传。
    • 纠正: 虽然学术定义如此,但在 JS 工程实践中(如 Lodash 的 _.curry),通常支持“宽泛柯里化”,即支持 f(a, b)(c) 这种混合传参方式,上面的通用实现代码也体现了这一点。
  • 误区三:arguments 的使用。
    • 纠正: 在写通用柯里化函数时,尽量使用 ES6 的剩余参数(Rest Parameters, ...args,而不是老旧的 arguments 对象,这样代码更现代、更易读。

总结

柯里化是前端进阶的一道分水岭。

  • 初级开发者看到的是繁琐的括号嵌套;
  • 高级开发者看到的是参数的预填充逻辑的延迟执行

在面试中,当你不仅能手写通用的 curry 函数,还能结合业务场景(如表单校验、日志系统、权限控制)侃侃而谈时,你就已经从一个“API调用者”进阶为一名具备工程化思维的“前端工程师”了。