函数柯里化 (Currying)
1. 什么是柯里化?
柯里化(Currying)是函数式编程这里的一个重要概念。它是指将一个接受多个参数的函数,将其转换为一系列接受单一参数的函数的过程。
简单来说,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
$$ f(a, b, c) \rightarrow f(a)(b)(c) $$
2. 为什么要使用柯里化?
柯里化主要有以下几个核心优势:
2.1 参数复用
当在多次调用中,某些参数是相同的,柯里化可以帮助我们固定这些参数,生成一个新的函数,避免重复传递相同的参数。
2.2 延迟执行
柯里化的函数不会立即执行,而是不断地收集参数,直到参数数量满足要求时才会执行真正的逻辑。这也可以看作是一种"预加载"函数。
2.3 函数组合的基础
在函数组合(Composition)中,我们需要单参数的函数(Unary Function)。柯里化可以将多元函数转化为一元函数,使其能够顺畅地融入函数管道(Pipe)中。
3. 代码实现
3.1 简单实现 (手动嵌套)
最基础的柯里化就是利用闭包返回函数。
javascript
// 普通函数
function add(x, y) {
return x + y;
}
// 柯里化后的函数
function curriedAdd(x) {
return function(y) {
return x + y;
}
}
// 使用箭头函数简化
const addES6 = x => y => x + y;
add(1, 2); // 3
curriedAdd(1)(2); // 33.2 通用实现 (自动柯里化)
我们可以编写一个通用的 curry 函数,将任何普通函数转换为柯里化函数。
javascript
function curry(fn) {
// 1. 获取原函数需要的参数个数
const arity = fn.length;
return function curried(...args) {
// 2. 如果当前收集的参数个数 >= 原函数需要的参数个数
if (args.length >= arity) {
// 执行原函数
return fn.apply(this, args);
} else {
// 3. 否则返回一个新函数,继续收集剩余的参数
return function (...nextArgs) {
// 将之前的参数(args)和新的参数(nextArgs)合并,递归调用 curried
return curried.apply(this, args.concat(nextArgs));
};
}
};
}使用示例:
javascript
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
curriedSum(1, 2, 3); // 6
curriedSum(1)(2, 3); // 6
curriedSum(1)(2)(3); // 64. 实际应用场景
4.1 正则校验工具库
假设我们有一个通用的正则校验函数 checkByRegExp,我们可以通过柯里化衍生出各种特定的校验函数。
javascript
const checkByRegExp = (regExp, string) => regExp.test(string);
const curriedCheck = curry(checkByRegExp);
// 生成特定的校验函数
const isMobile = curriedCheck(/^1\d{10}$/);
const isEmail = curriedCheck(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
// 使用
isMobile('13800138000'); // true
isEmail('test@example.com'); // true4.2 DOM 事件监听兼容处理
在处理浏览器兼容性时,我们可以利用柯里化的延迟执行特性,只在第一次调用时进行环境判断,后续调用直接使用绑定好的函数。
javascript
const addEvent = (function() {
if (window.addEventListener) {
return function(element, type, listener, useCapture) {
element.addEventListener(type, function(e) {
listener.call(element, e);
}, useCapture);
};
} else if (window.attachEvent) {
return function(element, type, listener) {
element.attachEvent("on" + type, function(e) {
listener.call(element, e);
});
};
}
})();
// 这里虽然是一个立即执行函数返回的闭包,也体现了类似柯里化的思想:预先固定某些环境参数或逻辑。4.3 配合 Map/Filter/Reduce
在处理数组时,柯里化函数非常有用,尤其是当使用 Point-free 风格编程时。
javascript
const add = x => y => x + y;
const multiply = x => y => x * y;
const arr = [1, 2, 3];
// 传统的写法
const result1 = arr.map(item => item * 2).map(item => item + 1);
// 使用柯里化函数
// 注意:map 需要一个函数作为参数,multiply(2) 返回的正是这样一个函数
const result2 = arr.map(multiply(2)).map(add(1));
console.log(result2); // [3, 5, 7]5. 常见面试题:实现 add(1)(2)(3)
题目:实现一个 add 函数,满足 add(1)(2)(3) 返回 6,并且能够处理任意长度的调用。
思路:这通常涉及到重写函数的 toString 或 valueOf 方法,以便在最后求值时返回结果。
javascript
function addInfinite(...args) {
// 内部函数,负责收集参数
const _adder = function(...nextArgs) {
return addInfinite(...args, ...nextArgs);
};
// 重写 valueOf 或 toString,用于在隐式转换时计算结果
_adder.toString = function() {
return args.reduce((a, b) => a + b, 0);
};
return _adder;
}
// 注意:结果是一个函数,但在 alert 或 console.log (某些环境) 或者是参与计算时会调用 toString
// console.log(+addInfinite(1)(2)(3)); // 6 (通过 + 触发隐式转换)6. 总结
- 柯里化是将多参数函数转换为单参数函数序列的过程。
- 它有助于参数复用,提高代码的模块化程度。
- 它是函数组合的重要基础。
- 实现的关键在于闭包和参数收集。