【前端面试小册】JS-第31节:实现 curry 函数柯里化
一、核心概念
1.1 什么是柯里化
**柯里化(Currying)**是一种将多参数函数转换为单参数函数序列的技术。
简单理解:将一个接受多个参数的函数,转换为接受一个参数并返回一个新函数的过程。
1.2 基本示例
// 普通函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化后
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
1.3 柯里化的优点
- 减少冗余代码:可以复用部分参数
- 增加可读性:函数调用更清晰
- 函数组合:便于函数式编程
- 参数复用:可以创建特定用途的函数
二、基础实现
2.1 实现代码
function curry(fn, currArgs) {
return function(...args) {
// 首次调用时,若未提供最后一个参数 currArgs,则不用进行 args 的拼接
if (currArgs !== undefined) {
args = args.concat(currArgs);
}
// 递归调用
if (args.length < fn.length) {
return curry(fn, args);
}
return fn.apply(null, args);
};
}
2.2 原理解析
执行流程:
graph TD
A[调用 curry fn] --> B[返回新函数]
B --> C[调用新函数传入 args]
C --> D{是否有 currArgs?}
D -->|是| E[合并参数]
D -->|否| F[使用当前 args]
E --> G{参数数量是否足够?}
F --> G
G -->|否| H[递归调用 curry]
G -->|是| I[执行原函数]
H --> B
关键点:
- 使用闭包保存已收集的参数
- 递归调用直到参数数量足够
- 使用
fn.length判断参数数量
2.3 测试用例
function sum(a, b, c) {
return a + b + c;
}
const fn = curry(sum);
let res1 = fn(1, 2, 3); // 6
let res2 = fn(1, 2)(3); // 6
let res3 = fn(1)(2, 3); // 6
let res4 = fn(1)(2)(3); // 6
console.log(res1, res2, res3, res4); // 6 6 6 6
三、优化版本
3.1 支持占位符
function curry(fn, currArgs = []) {
return function(...args) {
// 合并参数
const allArgs = [...currArgs];
// 处理占位符
args.forEach(arg => {
const placeholderIndex = allArgs.indexOf(curry.placeholder);
if (placeholderIndex !== -1) {
allArgs[placeholderIndex] = arg;
} else {
allArgs.push(arg);
}
});
// 检查是否有占位符
const hasPlaceholder = allArgs.includes(curry.placeholder);
const validArgsLength = allArgs.filter(arg => arg !== curry.placeholder).length;
if (validArgsLength < fn.length && hasPlaceholder) {
return curry(fn, allArgs);
}
if (validArgsLength >= fn.length) {
return fn.apply(null, allArgs.slice(0, fn.length));
}
return curry(fn, allArgs);
};
}
// 占位符
curry.placeholder = Symbol('placeholder');
// 使用
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
const _ = curry.placeholder;
curriedSum(_, 2)(1, 3); // 6
3.2 支持无限参数
function curry(fn, currArgs = []) {
return function(...args) {
const allArgs = currArgs.concat(args);
// 如果没有指定参数数量,或者参数数量足够,执行函数
if (fn.length === 0 || allArgs.length >= fn.length) {
return fn.apply(this, allArgs);
}
return curry(fn, allArgs);
};
}
// 使用
function sum(...args) {
return args.reduce((a, b) => a + b, 0);
}
const curriedSum = curry(sum);
curriedSum(1)(2)(3)(4)(5); // 15
3.3 完整版本
function curry(fn, currArgs = []) {
return function(...args) {
// 合并参数
const allArgs = [...currArgs, ...args];
// 如果参数数量足够,执行函数
if (allArgs.length >= fn.length) {
return fn.apply(this, allArgs);
}
// 否则继续收集参数
return curry(fn, allArgs);
};
}
// 支持 this 绑定
function curryWithContext(fn, currArgs = []) {
return function(...args) {
const allArgs = [...currArgs, ...args];
if (allArgs.length >= fn.length) {
return fn.apply(this, allArgs);
}
return curryWithContext(fn.bind(this), allArgs);
};
}
四、实际应用场景
4.1 参数复用
// 普通函数
function add(a, b) {
return a + b;
}
// 柯里化后
const curriedAdd = curry(add);
const add10 = curriedAdd(10);
console.log(add10(5)); // 15
console.log(add10(20)); // 30
4.2 事件处理
// 普通函数
function handleEvent(type, element, callback) {
element.addEventListener(type, callback);
}
// 柯里化后
const curriedHandle = curry(handleEvent);
const handleClick = curriedHandle('click');
const handleMouseOver = curriedHandle('mouseover');
handleClick(button, () => console.log('clicked'));
handleMouseOver(div, () => console.log('hovered'));
4.3 API 请求
// 普通函数
function request(method, url, data) {
return fetch(url, {
method,
body: JSON.stringify(data)
});
}
// 柯里化后
const curriedRequest = curry(request);
const get = curriedRequest('GET');
const post = curriedRequest('POST');
get('/api/users');
post('/api/users', { name: '愚公上岸说' });
4.4 函数组合
// 组合函数
const compose = (...fns) => (value) =>
fns.reduceRight((acc, fn) => fn(acc), value);
// 柯里化工具函数
const map = curry((fn, arr) => arr.map(fn));
const filter = curry((fn, arr) => arr.filter(fn));
const reduce = curry((fn, init, arr) => arr.reduce(fn, init));
// 使用
const numbers = [1, 2, 3, 4, 5];
const double = map(x => x * 2);
const even = filter(x => x % 2 === 0);
const sum = reduce((a, b) => a + b, 0);
const result = compose(sum, even, double)(numbers);
console.log(result); // 12
五、与偏函数的区别
5.1 偏函数(Partial Application)
// 偏函数:固定部分参数
function partial(fn, ...fixedArgs) {
return function(...args) {
return fn(...fixedArgs, ...args);
};
}
// 使用
function add(a, b, c) {
return a + b + c;
}
const add10 = partial(add, 10);
console.log(add10(5, 3)); // 18
5.2 区别对比
| 特性 | 柯里化 | 偏函数 |
|---|---|---|
| 参数数量 | 每次一个 | 可以多个 |
| 返回值 | 新函数 | 新函数 |
| 灵活性 | 更灵活 | 相对固定 |
六、面试要点总结
核心知识点
- 柯里化定义:多参数函数转换为单参数函数序列
- 实现原理:闭包 + 递归
- 优点:参数复用、代码复用、函数组合
- 应用场景:参数复用、事件处理、API 封装
常见面试题
Q1: 什么是函数柯里化?
答:柯里化是将多参数函数转换为单参数函数序列的技术。每次调用返回一个新函数,直到参数数量足够时执行原函数。
Q2: 如何实现柯里化?
答:使用闭包保存已收集的参数,递归调用直到参数数量足够,使用 fn.length 判断参数数量。
Q3: 柯里化的优点?
答:
- 减少冗余代码
- 增加可读性
- 参数复用
- 便于函数组合
实战建议
- ✅ 理解柯里化的原理和实现
- ✅ 掌握闭包和递归的使用
- ✅ 在实际项目中合理使用柯里化
- ✅ 注意与偏函数的区别
前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
