JavaScript 柯里化完全指南:闭包 + 手写 curry,一步步拆解原理

在 JavaScript 中,函数是开发者手中的多面利器。除了完成基本的功能逻辑,函数还能充当"参数工厂"、"函数生成器"甚至是"状态管理者"。今天我们要聊的,是一个在函数式编程中非常重要的技巧------柯里化(Currying)

很多人第一次看到 add(1)(2)(3) 这种写法可能会一头雾水,这究竟是啥操作?其实这就是柯里化的典型用法。本文将带你从基础概念到源码实现,再到实际应用场景,一步步拆解柯里化的底层原理和它与闭包的关系。


什么是柯里化?

简单来说,柯里化是把一个多参数函数,转换成一系列接收单一参数函数的技术,并在收集完所有参数后执行。

举个例子:

js 复制代码
function add(a, b, c) {
  return a + b + c;
}

// 普通调用
add(1, 2, 3); // 6

// 柯里化后
addCurry(1)(2)(3); // 6

这看起来像是魔法,其实背后全靠函数嵌套 + 闭包保存状态。


柯里化和闭包到底有啥关系?

闭包的定义是:函数可以"记住"并访问它定义时的作用域,即使它在当前词法环境之外执行。

换句话说,外层函数的变量可以被内层函数记住。这正是柯里化能够"逐步记住参数"的关键。

示例:

js 复制代码
function outer(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

outer(1)(2)(3); // 6

每一层函数都"闭住"了上一层的参数值。通过这种方式,参数一个个"被记住",最后一次性用于求值。


手写 curry 函数:核心实现逻辑

我们希望写一个通用的 curry 函数,能把任何多参函数转换成柯里化函数。核心思路如下:

  1. 判断参数是否收集完毕
  2. 如果没收完,就继续返回函数收集剩余参数
  3. 收完了就执行原始函数

实现代码如下:

js 复制代码
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args); // 参数收集完毕,执行原函数
    } else {
      return function (...newArgs) {
        return curried(...args, ...newArgs); // 继续收集参数
      };
    }
  };
}

测试一下:

js 复制代码
function add(a, b, c) {
  return a + b + c;
}

const addCurry = curry(add);

console.log(addCurry(1)(2)(3));       // 6
console.log(addCurry(1, 2)(3));       // 6
console.log(addCurry(1)(2, 3));       // 6

柯里化函数不仅能一个参数一个参数传,也支持一次传多个,非常灵活。


curry 函数拆解讲解

js 复制代码
function curried(...args) {
  if (args.length >= fn.length) {
    return fn(...args);
  }
  return (...newArgs) => curried(...args, ...newArgs);
}
  • fn.length 是原始函数需要的参数个数;
  • args 是目前已经收集到的参数;
  • 如果不够,就返回一个新函数继续收;
  • 最终收齐后再一次性调用 fn(...args) 执行。

通过不断嵌套返回函数,我们可以把一次性的参数收集,变成多次逐步收集------这就是柯里化。


为什么不直接用 arguments?

你可能会问,我用 arguments...args 直接收参数不行吗?

js 复制代码
function add() {
  let result = 0;
  for (let i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
}

这种写法确实能处理不定参数,但它不是柯里化 ,也不能让你优雅地 add(1)(2)(3)。柯里化强调的是函数返回函数 ,是一种延迟执行 + 收集参数的机制。

拓展阅读:函数对象 & arguments 参数处理机制

在 JavaScript 中,函数本身其实也是对象。每个函数都附带一些属性,比如:

js 复制代码
function add(a, b, c) {}
console.log(add.length); // 输出 3,表示期望的参数个数

这里的 add.length 并不是函数的功能输出,而是函数声明时定义的形参个数,这在我们实现 curry 函数时非常重要 ------ 它告诉我们应该收集多少个参数。


arguments:函数中的参数"总管"

我们可以通过一个例子来看 arguments 是什么:

js 复制代码
function add() {
  console.log(arguments, arguments.length); // [Arguments] { '0': 1, '1': 2, '2': 3 }  3
  console.log(Object.prototype.toString.call(arguments)); // [object Arguments] 
  const args = Array.from(arguments); // 转为真正的数组
  console.log(Object.prototype.toString.call(args)); // [object Array]

  let result = 0;
  for (let i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  console.log(result);
}

add(1, 2, 3); // 输出 6

🔍 解释几点:

  • arguments 是一个类数组对象 ,它具有 .length 属性,也可以用索引访问每个参数。
  • 但它 不是 真正的数组,不能直接使用 .map().filter() 等数组方法。
  • 我们通常通过 Array.from(arguments)[...arguments](ES6 展开运算符)来将其转化为标准数组。

柯里化的实际应用场景

1. 提前"预处理"参数

js 复制代码
function checkLength(min, max, str) {
  return str.length >= min && str.length <= max;
}

const check6to12 = curry(checkLength)(6, 12);
check6to12('abc');     // false
check6to12('password'); // true

这就像是做了一个"定制版"的校验函数。

2. 函数组合时更自然

js 复制代码
const multiply = curry((a, b) => a * b);
const double = multiply(2);
double(5); // 10

你可以根据场景固定某些参数,获得更语义化的函数。


总结

柯里化不仅是一个函数技巧,更是一种思维方式:通过拆解参数,实现延迟执行与更强的复用性

回顾要点:

  • 柯里化 ≠ 不定参数
  • 它通过闭包逐步收集参数
  • 参数收集完毕才执行原函数
  • 有助于提升代码可读性、组合性与复用性

彩蛋:用一句话记住 curry 原理

"我先记着你这几个参数,等人到齐了我就干活。"


如果你对函数式编程感兴趣,柯里化绝对是一个值得深入实践的核心概念。建议你自己手写几遍 curry 函数,尝试用它封装一些日常函数,你会发现它能带来很多意想不到的写法优化。

相关推荐
hweiyu009 分钟前
HTML5 和 CSS3 简介及核心特性(附电子书资料)
前端·css3·html5
枷锁—sha11 分钟前
【DVWA系列】——xss(DOM)——High详细教程
前端·web安全·网络安全·xss
o0向阳而生0o34 分钟前
69、JS中如何调用上位机接口
javascript·上位机
代码老y43 分钟前
前端开发中的可访问性设计:让互联网更包容
java·服务器·前端·数据库
白总Server1 小时前
Golang dig框架与GraphQL的完美结合
java·大数据·前端·javascript·后端·go·graphql
m0_679927201 小时前
练习小项目11:鼠标跟随小圆点
前端·javascript·css·html
只可远观2 小时前
Flutter Android打包和发布Build APK
前端·flutter·dart
BillKu2 小时前
Vue3 + TypeScript 操作第三方库(Element Plus 的 ElTable)的内部属性
前端·javascript·typescript
嘉小华2 小时前
深入浅出Android SurfaceView:高性能绘制
前端
古夕2 小时前
webpack 之 Tree-shaking
前端·面试·webpack