实现一个JS函数柯里化

手写实现函数柯里化,将多参数函数转换为单参数函数的嵌套调用形式

问题

函数柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。简单来说,就是将 f(a, b, c) 转换为 f(a)(b)(c) 的形式。

需要实现一个通用的柯里化函数,能够将任意多参数函数转换为柯里化形式。

解答

/**
 * 函数柯里化实现
 * @param {Function} fn - 需要柯里化的函数
 * @param {Array} args - 已经收集的参数
 * @returns {Function} 柯里化后的函数
 */
function curry(fn, ...args) {
  // 获取函数的参数个数
  const length = fn.length;
  
  return function(...newArgs) {
    // 合并已收集的参数和新传入的参数
    const allArgs = [...args, ...newArgs];
    
    // 如果参数个数足够,则执行原函数
    if (allArgs.length >= length) {
      return fn.apply(this, allArgs);
    } else {
      // 否则继续返回柯里化函数,收集参数
      return curry.call(this, fn, ...allArgs);
    }
  };
}

// 方法二:更简洁的实现
function curry2(fn) {
  return function curried(...args) {
    // 参数够了就执行,不够就继续返回函数
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...newArgs) {
        return curried.apply(this, [...args, ...newArgs]);
      };
    }
  };
}

// 方法三:支持占位符的柯里化
function curryWithPlaceholder(fn, placeholder = '_') {
  return function curried(...args) {
    // 检查是否有占位符,以及参数是否足够
    const hasPlaceholder = args.some(arg => arg === placeholder);
    
    if (!hasPlaceholder && args.length >= fn.length) {
      return fn.apply(this, args);
    }
    
    return function(...newArgs) {
      // 替换占位符
      const finalArgs = args.map(arg => 
        arg === placeholder && newArgs.length ? newArgs.shift() : arg
      );
      // 添加剩余的新参数
      return curried.apply(this, [...finalArgs, ...newArgs]);
    };
  };
}

使用示例

// 示例1:基础使用
function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

// 示例2:实际应用场景 - 日志函数
function log(level, time, message) {
  console.log(`[${level}] ${time}: ${message}`);
}

const curriedLog = curry(log);
const errorLog = curriedLog('ERROR');
const errorLogNow = errorLog(new Date().toISOString());

errorLogNow('用户登录失败'); // [ERROR] 2024-01-01T00:00:00.000Z: 用户登录失败
errorLogNow('数据库连接失败'); // [ERROR] 2024-01-01T00:00:00.000Z: 数据库连接失败

// 示例3:数学计算
function multiply(a, b, c) {
  return a * b * c;
}

const curriedMultiply = curry(multiply);
const double = curriedMultiply(2);
const doubleThenTriple = double(3);
console.log(doubleThenTriple(4)); // 24

// 示例4:使用占位符
const curriedAddWithPlaceholder = curryWithPlaceholder(add);
const add5 = curriedAddWithPlaceholder('_', '_', 5);
console.log(add5(1, 2)); // 8
console.log(add5(3, 4)); // 12

关键点

  • 参数收集:通过闭包保存已经传入的参数,每次调用时将新参数与旧参数合并

  • 参数判断:使用 fn.length 获取原函数的参数个数,当收集的参数数量达到要求时执行原函数

  • 递归调用:参数不足时返回新的柯里化函数,继续收集参数,形成链式调用

  • this 绑定:使用 applycall 确保函数执行时 this 指向正确

  • 灵活调用:支持一次传入多个参数,如 f(1,2)(3)f(1)(2)(3) 都能正常工作

  • 扩展功能:可以实现占位符功能,允许跳过某些参数位置,提供更灵活的参数传递方式

  • 应用场景:参数复用、延迟执行、动态生成函数等场景,特别适合函数式编程风格