Skip to content

🔥自动柯里化:如何让普通函数支持 f(1,2,3)f(1)(2)(3)

——利用闭包与参数长度检测实现灵活调用

“柯里化不是目的,
灵活性才是。”

一、问题:我们想要“两种调用方式都行”

js
const add = (a, b, c) => a + b + c;

// 我们希望它同时支持:
add(1, 2, 3);     // ✅ 经典调用
add(1)(2)(3);     // ✅ 柯里化调用
add(1, 2)(3);     // ✅ 混合调用
add(1)(2, 3);     // ✅ 混合调用

这叫 自动柯里化(Auto-currying)智能柯里化

二、核心思路

  1. 获取函数的期望参数数量fn.length
  2. 收集参数,直到数量足够
  3. 利用闭包保存已传参数
  4. 参数够了就执行,否则返回新函数继续收

三、实现一个通用 autoCurry

js
const autoCurry = (fn) => {
  const arity = fn.length; // 函数期望的参数个数

  const makeCurried = (collectedArgs = []) => {
    return (...args) => {
      const newArgs = [...collectedArgs, ...args];
      
      // 参数够了,直接执行
      if (newArgs.length >= arity) {
        return fn(...newArgs);
      }
      
      // 不够,返回函数继续收集
      return makeCurried(newArgs);
    };
  };

  return makeCurried();
};

四、测试我们的 autoCurry

js
const add = (a, b, c) => a + b + c;
const curriedAdd = autoCurry(add);

curriedAdd(1, 2, 3);     // 6
curriedAdd(1)(2)(3);     // 6
curriedAdd(1, 2)(3);     // 6
curriedAdd(1)(2, 3);     // 6
curriedAdd()(1, 2, 3);   // 6

全部通过!

五、进阶:支持默认参数和 rest 参数?

问题:fn.length 不包含默认值或 rest 参数

js
const fn = (a, b = 1, ...rest) => {};
fn.length; // 1 —— 只算必传参数

在自动柯里化中,这可能导致“提前执行”。

解决方案(可选):

  • 要求用户传入 arity 手动指定
  • 使用 AST 分析(复杂,运行时成本高)

实践中:大多数工具函数无默认/rest,fn.length 足够。

六、真实应用:让 Lodash/Underscore 函数自动柯里化

js
import { map, filter } from 'lodash';

const cMap = autoCurry(map);
const cFilter = autoCurry(filter);

// 现在可以这样用:
const doubleAll = cMap(x => x * 2);
const evensOnly = cFilter(x => x % 2 === 0);

[1,2,3] |> doubleAll |> evensOnly; // [4, 6]

七、优化:缓存中间函数(性能考虑)

每次调用都创建新函数,可能影响性能。

我们可以缓存常见参数组合:

js
const autoCurry = (fn) => {
  const arity = fn.length;
  const cache = new WeakMap(); // 或普通对象

  const makeCurried = (collectedArgs = []) => {
    // 尝试从缓存读取
    if (cache.has(collectedArgs)) {
      return cache.get(collectedArgs);
    }

    const curried = (...args) => {
      const newArgs = [...collectedArgs, ...args];
      if (newArgs.length >= arity) {
        return fn(...newArgs);
      }
      return makeCurried(newArgs);
    };

    // 缓存
    cache.set(collectedArgs, curried);
    return curried;
  };

  return makeCurried();
};

适用于高频调用的函数。

八、工程建议:何时使用自动柯里化?

推荐场景

  • 工具函数库(如自定义 utils.js
  • 函数组合(pipe / compose
  • 领域特定语言(DSL)
  • React 的 connect / withStyles 类型函数

不推荐场景

  • 性能关键路径(闭包有开销)
  • 构造函数或类方法
  • 参数复杂的函数(易混淆)

九、对比:手动柯里化 vs 自动柯里化

方式优点缺点
手动柯里化
const add = a => b => c => a+b+c
精确控制,性能好冗长,不支持多参调用
自动柯里化灵活,兼容旧代码运行时判断,稍慢

推荐:在业务项目中用自动柯里化,平衡灵活性与简洁性。

十、结语:柯里化的真正价值是“部分应用”

自动柯里化的核心价值不是语法炫技,而是:

让你轻松创建“预配置”函数。

js
const urlFor = autoCurry((domain, path, query) =>
  `${domain}${path}?${new URLSearchParams(query)}`
);

const apiCall = urlFor('https://api.example.com');
const getUser = apiCall('/users');
const user1 = getUser({ id: 1 }); 
// 'https://api.example.com/users?id=1'

这才是函数式编程的优雅所在:
通过组合和部分应用,构建领域语言。