函数柯里化 (Function Currying)
📖 什么是函数柯里化 (What is Function Currying?)
函数柯里化 (Currying) 是函数式编程 (Functional Programming) 中的一个重要概念。
它的核心思想是:将一个接收多个参数的函数,转换成一系列只接收一个参数的函数。
简单来说,就是把一个多参数函数 f(a, b, c) 拆分成 f(a)(b)(c) 的形式。每次调用只传递一个参数,函数会返回一个新的函数,等待接收下一个参数,直到所有参数都接收完毕,最后返回最终的计算结果。
这个技术以逻辑学家 Haskell Curry 的名字命名。
核心定义:把接收多个参数的函数,变换成接收一个单一参数的(并返回新函数的)函数。
🚀 快速入门示例 (Quick Start Example)
我们来看一个最简单的例子,将一个接收两个数字并求和的函数进行柯里化。
柯里化前
这是一个普通的接收多参数的函数。
// 接收多个参数的函数
function add(x, y) {
return x + y;
}
console.log(add(1, 2)); // 输出: 3柯里化后
柯里化后,函数每次只接收一个参数,并返回一个新函数来接收下一个参数。
// 柯里化后的函数
function curriedAdd(x) {
// 返回一个新函数,利用闭包记住了 x 的值
return function(y) {
return x + y;
};
}
const add1 = curriedAdd(1); // 固定了第一个参数为 1,返回一个新函数
const result = add1(2); // 传入第二个参数 2
console.log(result); // 输出: 3
// 也可以链式调用
console.log(curriedAdd(1)(2)); // 输出: 3在这个例子中,curriedAdd 利用了 闭包 的特性。当 curriedAdd(1) 被调用时,内部函数记住了外部作用域中的 x 的值(即 1),并被返回。当这个返回的函数再次被调用并传入 2 时,它就可以访问之前保存的 x 并完成 x + y 的计算。
🛠️ 柯里化的实际应用 (Practical Applications of Currying)
柯里化最大的好处是 参数复用 (或称 固定参数),它允许我们创建更专业、更具体的函数。
1. 参数复用 (Parameter Reuse)
在实际开发中,我们可能需要多次调用同一个函数,而其中某些参数是固定的。柯里化可以帮助我们避免重复传递这些相同的参数。
场景:创建一个函数来检查字符串是否符合某个正则表达式。
非柯里化实现
每次调用都需要同时传入正则表达式和待验证的字符串。
function check(reg, str) {
return reg.test(str);
}
check(/\d+/g, 'test'); // false
check(/\d+/g, '123'); // true
check(/\d+/g, '456'); // true
// 正则表达式被重复传递柯里化实现
我们可以先固定正则表达式,生成一个专门用于数字验证的新函数。
function curriedCheck(reg) {
return function(str) {
return reg.test(str);
}
}
// 固定正则表达式,创建一个专门用于检查数字的函数
const hasNumber = curriedCheck(/\d+/g);
// 后续调用只需传入变化的参数
console.log(hasNumber('test')); // false
console.log(hasNumber('123')); // true
console.log(hasNumber('abc')); // false通过柯里化,我们将一个通用函数 curriedCheck 变成了多个专用函数(如 hasNumber),使代码更具可读性和复用性。
2. 提前确认 (Early Confirmation)
柯里化可以用于提前执行某些逻辑,返回一个已经“确认”过环境的函数,从而避免在后续调用中进行重复的判断。
场景:封装一个跨浏览器的事件绑定函数,需要判断浏览器是支持 addEventListener 还是 attachEvent。
const on = (function() {
// 这个判断只会在代码加载时执行一次
if (document.addEventListener) {
// 返回一个已经确认好的函数
return function(element, event, handler) {
element.addEventListener(event, handler, false);
};
} else {
// 返回另一个已经确认好的函数
return function(element, event, handler) {
element.attachEvent('on' + event, handler);
};
}
})();
// 后续调用 on 函数时,不再需要进行 if/else 判断
// const btn = document.getElementById('myBtn');
// on(btn, 'click', () => { console.log('clicked!'); });这种模式(虽然是通过 IIFE 实现的,但思想与柯里化一致)通过一次性的环境嗅探,避免了每次调用事件绑定时都执行if/else判断,提高了运行效率。
⚙️ 封装一个通用的柯里化函数
为了避免为每个函数都手动编写柯里化版本,我们可以封装一个通用的 curry 函数。
这个 curry 函数接收一个待柯里化的函数作为参数,并返回一个新的柯里化后的函数。
function curry(fn, ...initialArgs) {
// 获取目标函数期望的参数个数
const functionLength = fn.length;
// 预先收集的参数数组
let args = initialArgs;
// 返回一个新函数,用于收集参数
return function _curry(...newArgs) {
// 将新传入的参数和之前的参数合并
args = [...args, ...newArgs];
// 判断当前收集的参数数量是否已达到目标函数所需数量
if (args.length >= functionLength) {
// 如果参数足够,则执行原函数并返回结果
return fn(...args);
} else {
// 如果参数不够,则返回自身,等待接收更多参数
return _curry;
}
};
}
// --- 测试 ---
// 1. 定义一个多参数函数
function add(a, b, c) {
return a + b + c;
}
// 2. 使用 curry 函数将其柯里化
const curriedAdd = curry(add);
// 3. 多种调用方式
console.log(curriedAdd(1)(2)(3)); // 输出: 6
console.log(curriedAdd(1, 2)(3)); // 输出: 6
console.log(curriedAdd(1)(2, 3)); // 输出: 6🧠 经典面试题
实现一个 add 函数,使其能够满足以下所有调用方式:
add(1)(2)(3) == 6;
add(1, 2)(3) == 6;
add(1)(2, 3) == 6;分析:这道题的核心在于,函数的参数数量是不固定的,并且需要有一种机制来在最后触发计算。我们可以利用函数链式调用收集参数,并通过重写 toString 或 valueOf 方法,在进行比较或类型转换时隐式地触发求和计算。
实现:
function add(...initialArgs) {
// 使用一个数组来存储所有传入的参数
let allArgs = [...initialArgs];
// 定义并返回一个内部函数,用于继续收集参数
function adder(...newArgs) {
allArgs.push(...newArgs);
// 关键:返回自身以实现链式调用
return adder;
}
// 重写 adder 函数的 toString 方法
// 当 adder 被尝试转换为字符串(如 alert, console.log)或原始值(如 == 比较)时,此方法会被调用
adder.toString = function() {
// 使用 reduce 计算所有参数的总和
return allArgs.reduce((sum, current) => sum + current, 0);
};
return adder;
}
// --- 测试 ---
console.log(add(1)(2)(3).toString()); // 6
console.log(add(1, 2)(3).toString()); // 6
console.log(add(1)(2, 3)(4).toString()); // 10
// 使用 == 进行隐式类型转换,触发 toString
const result = add(1)(2)(3);
console.log(result == 6); // true