【前端面试小册】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天学完,上岸银行总行!

全部评论

相关推荐

贪玩的布莱克许愿ss...:985Java后端,最近刚开始投简历基本没面试😵隔壁班同学前两周投的京东直接过了,给我看了面经基本只问了八股和项目,八股也挺简单的,这哥们暑假跟我一块在一家小厂实习过,我俩简历也都差不多,结果现在我投的京东还在泡池子😭找工作难道真是运气大于一切吗
双非有机会进大厂吗
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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