Skip to content

柯里化(Currying):为什么 add(1)(2)(3) 是合法的?

——将多参数函数转换为单参数函数链,实现参数的“预填充”

“柯里化不是语法糖,而是对‘函数应用’的重新思考。”

一、现象:add(1)(2)(3) 为何不报错?

js
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6

这行代码能运行,是因为:

  1. add(1) 返回一个函数 f1
  2. f1(2) 返回一个函数 f2
  3. f2(3) 返回结果 6

但这只是表象。
真正的秘密在于:柯里化改变了我们“看待函数”的方式。

二、什么是柯里化?—— 从“一次喂饱”到“逐步消化”

传统函数:多参数,一次性调用

js
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 必须一次性提供所有参数

柯里化函数:单参数,链式调用

js
const add = a => b => c => a + b + c;
add(1)(2)(3); // 参数可以分步提供

柯里化(Currying)是将一个多参数函数转换为一系列只接受单个参数的函数的过程。

f(a, b, c) 被转换为 f(a)(b)(c)

三、底层机制:闭包与 [[Environment]] 的魔法

为什么 add(1) 返回的函数还能记住 a=1?

答案是:闭包(Closure)

js
const add = a => {           // 第一层:捕获 a
  return b => {              // 第二层:捕获 b 和 a(通过 [[Environment]])
    return c => a + b + c;   // 第三层:捕获 c, b, a
  };
};

当 add(1) 执行时:

  • 创建新作用域,a = 1
  • 返回内部函数 b => c => a + b + c
  • 该函数的 [[Environment]] 指向包含 a=1 的词法环境

后续调用都能访问这个“记忆”。

四、柯里化的真正价值:参数的“预填充”(Partial Application)

柯里化的核心优势不是写法炫酷,而是延迟绑定参数,实现:

1. 函数特化(Specialization)

js
const multiply = a => b => a * b;
const double = multiply(2);   // 预填充 a=2
const triple = multiply(3);   // 预填充 a=3

double(5); // 10
triple(5); // 15

你不需要写 double(x) { return 2 * x } 这样的重复代码。

2. 上下文预置(Context Binding)

js
const urlFor = protocol => domain => resource =>
  `${protocol}://${domain}/${resource}`;

const httpsGithub = urlFor('https')('github.com');
httpsGithub('algebra');     // 'https://github.com/algebra'
httpsGithub('monad');       // 'https://github.com/monad'

urlFor 抽象了 URL 构建逻辑,你可以预置协议和域名,得到专用构造器。

3. 高阶函数的完美搭档

js
// map 接受单参数函数
[1, 2, 3].map(multiply(2)); // [2, 4, 6]

// 如果 multiply 不是柯里化,你得这样写:
[1, 2, 3].map(x => multiply(2, x));

柯里化让函数天然适配 map/filter/reduce 等高阶函数。

五、柯里化 vs 偏应用(Partial Application):本质区别

特性柯里化(Currying)偏应用(Partial Application)
形式f(a)(b)(c)f(a, b)
参数数量每次只接受一个参数可一次提供多个参数
返回值未完全应用时总是返回函数未完全应用时返回函数
目的数学上的函数分解实用性的参数预设

示例对比

js
// 柯里化
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3);

// 偏应用
const partialAdd = (a, b, c) => a + b + c;
const add5 = _.partial(partialAdd, 5); // 预设 a=5
add5(2, 3); // 10

柯里化是一种特殊的偏应用,但偏应用更灵活。

六、如何手动实现一个自动柯里化函数?

js
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return (...nextArgs) => curried(...args, ...nextArgs);
    }
  };
}

// 使用
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);

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

fn.length 返回函数期望的参数个数,curry 利用它判断是否收集完所有参数。

七、工程实践:柯里化的典型场景

1. 配置驱动的函数

js
const validate = rule => value =>
  rule.test(value);

const isEmail = validate(/@/);
const isPhone = validate(/\d{10}/);

2. 日志装饰器

js
const logger = prefix => message =>
  console.log(`[${prefix}] ${message}`);

const errorLog = logger('ERROR');
errorLog('File not found');

3. React 中的事件处理器

js
const handleClick = id => event =>
  dispatch({ type: 'DELETE', payload: id });

<button onClick={handleClick(5)}>Delete</button>

结语:柯里化是“函数式编程的杠杆”

它让你用少量通用函数,组合出大量专用行为。

add(1)(2)(3) 合法,不是因为 JS 语法允许,
而是因为函数可以携带环境、延迟执行、逐步应用。

这才是柯里化的灵魂:

把“何时提供参数”的控制权,交给调用者。