【前端面试小册】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 柯里化的优点

  1. 减少冗余代码:可以复用部分参数
  2. 增加可读性:函数调用更清晰
  3. 函数组合:便于函数式编程
  4. 参数复用:可以创建特定用途的函数

二、基础实现

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 区别对比

特性 柯里化 偏函数
参数数量 每次一个 可以多个
返回值 新函数 新函数
灵活性 更灵活 相对固定

六、面试要点总结

核心知识点

  1. 柯里化定义:多参数函数转换为单参数函数序列
  2. 实现原理:闭包 + 递归
  3. 优点:参数复用、代码复用、函数组合
  4. 应用场景:参数复用、事件处理、API 封装

常见面试题

Q1: 什么是函数柯里化?

答:柯里化是将多参数函数转换为单参数函数序列的技术。每次调用返回一个新函数,直到参数数量足够时执行原函数。

Q2: 如何实现柯里化?

答:使用闭包保存已收集的参数,递归调用直到参数数量足够,使用 fn.length 判断参数数量。

Q3: 柯里化的优点?

答:

  • 减少冗余代码
  • 增加可读性
  • 参数复用
  • 便于函数组合

实战建议

  • ✅ 理解柯里化的原理和实现
  • ✅ 掌握闭包和递归的使用
  • ✅ 在实际项目中合理使用柯里化
  • ✅ 注意与偏函数的区别
#前端面试##贝壳##银行##快手#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

11-12 18:18
济南大学 Java
本人是农村人,又加上读的是计算机专业,所以找工作对于父母来说帮不上什么忙,刚上大学那一会儿,父母希望我考研,毕竟谁不想自己的孩子是个研究生呢,但是现实总是很残酷的,上岸率低的可怜,让我望而却步,所以在大三下就开始找工作了。今年六月中旬,因为秋招害怕没有实习经历,所以开始投递实习,找了半个多月,还没有找到,七月开始放暑假了,突然一下子来了好几个实习offer,但是都是大连那边的,济南本地的这边都是很小的公司(感觉还带有诈骗性质的),所以不得不购买飞往大连的飞机,因为手上确实没有几个子,只能订凌晨的航班,就在定完航班的第二天,一个济南本地的一个中型外包给我发了实习offer,管住还开的比大连那两家多1k,所以取消了飞往大连的航班。在入职大连某大型外包之前,公司需要体检,我去体检还查出了吉尔伯特综合征,医生说这个是基因问题,还不能治疗。实习了小三个月,期间发了工资还给老妈换了手机,我在寻思是否在这个不错的外包干下去还是跑路秋招,父母也是全都支持我,说让我自己做选择,我也是跑路了,十月开始秋招,每天都是很焦虑,基本天天在自习室的外面跟家里打电话,不知道是什么原因,十月的济南比以往更冷了一些,可能是心寒吧,但最后还是找到了。回想起之前因为考研的事与父母顶嘴,妈妈说你考就行,你找不到工作我们还能供不起你吃吗?我说我没有考上你们也不能帮我找到工作呀(我认为考研和找心仪的工作只能二选一,而且第一份工作我觉得也很重要),这句话可能伤到了老妈,她没说什么了又,越长大才越发现,亲情与健康其实比什么都重要,不知道如果我考上了本校的研究生回事怎么样的人生,毕竟人总是会美化自己没有走过的道路。
工作以后,你父母对你啥态...
点赞 评论 收藏
分享
11-11 11:35
已编辑
字节跳动_字节hr
“面试官好,我之前在字节跳动实习过……”“您说的是字节跳动?”原本在低头圈画简历的三位面试官瞬间停了手,握着的笔顿在半空,会议室里只有键盘余温散出的微弱声响,都变得格外清晰。“就是那个靠算法推荐,把短视频和资讯信息流做到顶流的字节跳动?”我微微颔首,保持着平稳的语气:“嗯,主要在抖音后端研发团队实习,负责用户行为埋点链路优化、推荐策略效果迭代的工程落地,参与过流量峰值容灾方案设计和线上故障排查,把接口响应耗时压到了毫秒级。”其中一位面试官身体前倾,声音都亮了几分:“天啊!是那个做出抖音、TikTok,还把飞书、剪映做到行业标杆的字节跳动?”另一位面试官手忙脚乱摸出手机:“不好意思,我先拍个抖音,刚好想发条‘面到字节实习生’的动态蹭波流量。”话还没落地,最后一位面试官已经把一份写满薪资数字的&nbsp;offer&nbsp;意向书推到我面前:“现在就能签吗?股票期权随便谈,级别我们直接给你定到&nbsp;P6!”我抬眼扫过三人,礼貌而从容地回应:“Thank&nbsp;you&nbsp;very&nbsp;much&nbsp;for&nbsp;the&nbsp;offer.&nbsp;I&nbsp;still&nbsp;need&nbsp;to&nbsp;compare&nbsp;different&nbsp;opportunities,&nbsp;and&nbsp;I’ll&nbsp;get&nbsp;back&nbsp;to&nbsp;you&nbsp;once&nbsp;I’ve&nbsp;made&nbsp;my&nbsp;decision.”说完,我合上桌边一本印着&nbsp;“ByteDance”&nbsp;蓝色&nbsp;logo&nbsp;的笔记本,微笑点头后离开了会议室。走到走廊尽头时,我忍不住轻叹了口气:“唉,手里的&nbsp;offer&nbsp;都堆不下了,到底选哪个好呢?要是字节的技术口碑没这么硬就好了。”都看到这里了&nbsp;不投一下吗【字节跳动-校园招聘】内推链接:https://job.toutiao.com/s/KTaYC1Kvckg,内推码:MNW17D7。心脏和字节一起等你跳动~~~(通过此链接投递计入内推,内推简历优先筛选~)参考文献[1]&nbsp;&quot;老师好,我之前在蚂蚁集团实习过&quot;,&nbsp;国家一级产品交付工程师.[2]&nbsp;“面试官好,我之前在拼多多集团实习过……”,I_can_do_better.作者:I_can_do_better链接:https://www.nowcoder.com/来源:牛客网
投递字节跳动等公司10个岗位
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务