在 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
函数,能把任何多参函数转换成柯里化函数。核心思路如下:
- 判断参数是否收集完毕
- 如果没收完,就继续返回函数收集剩余参数
- 收完了就执行原始函数
实现代码如下:
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
函数,尝试用它封装一些日常函数,你会发现它能带来很多意想不到的写法优化。