从“炫技”到“利器”:前端面试官眼中的柯里化函数
在前端开发的面试江湖中,“柯里化”(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,后续只需要传b和c。 - 延迟执行: 函数不会立即求值,而是通过闭包把参数“存”起来,直到参数凑齐了(或者显式触发)才执行。
手写一个通用的柯里化函数
在面试中,只会写死 add(1)(2)(3) 是不够的。面试官通常会追问:“如果参数个数不确定怎么办?如果参数是分批传入的怎么办?”
这时,你需要拿出一个通用的 curry 工具函数。
核心逻辑:
- 利用
fn.length获取原函数需要的参数个数。 - 利用闭包保存已传入的参数。
- 利用递归判断参数是否凑齐:
- 如果没凑齐,返回一个新函数继续接参数。
- 如果凑齐了,执行原函数。
满分代码示例:
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)这种混合传参方式,上面的通用实现代码也体现了这一点。
- 纠正: 虽然学术定义如此,但在 JS 工程实践中(如 Lodash 的
- 误区三:
arguments的使用。- 纠正: 在写通用柯里化函数时,尽量使用 ES6 的剩余参数(Rest Parameters,
...args),而不是老旧的arguments对象,这样代码更现代、更易读。
- 纠正: 在写通用柯里化函数时,尽量使用 ES6 的剩余参数(Rest Parameters,
总结
柯里化是前端进阶的一道分水岭。
- 初级开发者看到的是繁琐的括号嵌套;
- 高级开发者看到的是参数的预填充和逻辑的延迟执行。
在面试中,当你不仅能手写通用的 curry 函数,还能结合业务场景(如表单校验、日志系统、权限控制)侃侃而谈时,你就已经从一个“API调用者”进阶为一名具备工程化思维的“前端工程师”了。