JavaScript函数柯里化:原理、应用与通用实现
本文详解JavaScript中的函数柯里化技术,解析其将多参数函数转换为单参数链式调用的原理。通过正则校验与事件监听两个典型案例,阐述柯里化在参数复用、提前返回和延迟执行方面的核心优势,并提供通用的柯里化工具函数封装方案。
1 什么是函数柯里化
在计算机科学领域,Currying技术指的是将接收多个参数的函数,转换为接收单一参数(即原函数的第一个参数)的函数,并返回一个接收剩余参数且能产生最终结果的新函数的过程。该概念以逻辑学家Haskell Curry 的名字命名。
通俗来说,柯里化是一种改造多参数函数的技术手段。
例如:
// 这是一个接受3个参数的函数
const add = function(x, y, z) {
return x + y + z
}
经过变换后,我们可以得到如下形式的函数:
// 接收一个单一参数
const curryingAdd = function(x) {
// 并且返回接受余下的参数的函数
return function(y, z) {
return x + y + z
}
}
这种变换在调用方式上有何不同?对比可见:
// 调用add
add(1, 2, 3)
// 调用curryingAdd
curryingAdd(1)(2, 3)
// 看得更清楚一点,等价于下面
const fn = curryingAdd(1)
fn(2, 3)
可以看出,变换后的函数允许分批接收参数。先记住这一特性,后续将介绍其实际用途。此外,curryingAdd所返回的函数还可以继续进行柯里化变换。
具体示例如下:
const curryingAdd = function(x) {
return function(y) {
return function(z) {
return x + y + z
}
}
}
// 调用
curryingAdd(1)(2)(3)
// 即
const fn = curryingAdd(1)
const fn1 = fn(2)
fn1(3)
上述两次变换的过程,即为函数柯里化的具体体现。
简而言之,柯里化是将一个多参数函数f转换为接受部分参数的函数g,该函数g会返回一个新函数h(记为h),由h来接收其余参数。函数h可以继续被柯里化,这就像一个套娃过程。
那么,进行如此复杂的函数柯里化究竟有何用途?
2 柯里化的作用和特点
2.1 参数复用
在实际开发中,常遇到通过正则表达式校验电话号码、邮箱或身份证号码合法性等需求。
因此,我们通常会封装一个校验函数,如下所示:
/**
* @description 通过正则校验字符串
* @param {RegExp} regExp 正则对象
* @param {String} str 待校验字符串
* @return {Boolean} 是否通过校验
*/
function checkByRegExp(regExp, str) {
return regExp.test(str)
}
如果我们需要校验大量手机号或邮箱,调用方式可能如下:
// 校验手机号
checkByRegExp(/^1\d{10}$/, '15152525634');
checkByRegExp(/^1\d{10}$/, '13456574566');
checkByRegExp(/^1\d{10}$/, '18123787385');
// 校验邮箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fsds@163.com');
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fdsf@qq.com');
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fjks@qq.com');
虽然看似没有错误,但事实上仍有改进空间。
- 在校验同类数据时,相同的正则表达式被重复编写了多次。
- 代码可读性较差,若无注释说明,很难一眼看出正则表达式的具体作用。
我们尝试使用函数柯里化来优化这段代码:
// 将函数柯里化
function checkByRegExp(regExp) {
return function(str) {
return regExp.test(str)
}
}
通过传入不同的正则对象,我们可以得到功能各异的函数:
// 校验手机
const checkPhone = curryingCheckByRegExp(/^1\d{10}$/)
// 校验邮箱
const checkEmail = curryingCheckByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)
现在校验手机号和邮箱的代码变得简洁,且可读性显著增强。
// 校验手机号
checkPhone('15152525634');
checkPhone('13456574566');
checkPhone('18123787385');
// 校验邮箱
checkEmail('fsds@163.com');
checkEmail('fdsf@qq.com');
checkEmail('fjks@qq.com');
这就是参数复用的体现:我们只需复用第一个参数regExp,即可直接调用具有特定功能的函数。
通用函数(如checkByRegExp)虽然解决了兼容性问题,但也带来了使用上的不便,例如不同应用场景需要传递多个不同参数才能解决问题。
有时,同一种规则可能会被反复使用(例如校验手机号的参数),这导致了代码冗余。利用柯里化可以消除重复,实现参数的复用。
柯里化的一个核心思想是:降低适用范围,从而提高适用性。
2.2 提前返回
在JS DOM事件监听程序中,我们使用addEventListener方法为元素添加事件处理程序。然而,部分旧版浏览器不支持此方法,此时我们会使用attachEvent方法来替代。
这时,我们通常会编写如下兼容各浏览器版本的代码:
/**
* @description:
* @param {object} element DOM元素对象
* @param {string} type 事件类型
* @param {Function} fn 事件处理函数
* @param {boolean} isCapture 是否捕获
* @return {void}
*/
function addEvent(element, type, fn, isCapture) {
if (window.addEventListener) {
element.addEventListener(type, fn, isCapture)
} else if (window.attachEvent) {
element.attachEvent("on" + type, fn)
}
}
我们使用addEvent来添加事件监听,但每次调用该方法时,都会进行一次兼容性判断。事实上,一旦浏览器版本确定,就不再进行重复判断。
通过柯里化处理:
function curryingAddEvent() {
if (window.addEventListener) {
return function(element, type, fn, isCapture) {
element.addEventListener(type, fn, isCapture)
}
} else if (window.attachEvent) {
return function(element, type, fn) {
element.attachEvent("on" + type, fn)
}
}
}
const addEvent = curryingAddEvent()
// 也可以用立即执行函数将上述代码合并
const addEvent = (function curryingAddEvent() {
...
})()
现在得到的addEvent是经过判断后返回的函数,后续调用无需再进行重复判断。
这就是提前返回(或称提前确认)。函数柯里化后可以提前处理部分任务,并返回一个处理其他任务的函数。
另外,我们可以看到,curryingAddEvent似乎并未接受参数。这是因为原函数的条件(即浏览器是否支持addEventListener)是直接从全局环境中获取的。
从逻辑上讲,其实可以改为:
let mode = window.addEventListener ? 0 : 1;
function addEvent(mode, element, type, fn, isCapture) {
if (mode === 0) {
element.addEventListener(type, fn, isCapture);
} else if (mode === 1) {
element.attachEvent("on" + type, fn);
}
}
// 这样柯里化后就可以先接受一个参数了
function curryingAddEvent(mode) {
if (mode === 0) {
return function(element, type, fn, isCapture) {
element.addEventListener(type, fn, isCapture)
}
} else if (mode === 1) {
return function(element, type, fn) {
element.attachEvent("on" + type, fn)
}
}
}
当然,这种改法并非必要。
2.3 延迟执行
事实上,上述正则校验和事件监听的例子中已经体现了延迟执行的特性。
curryingCheckByRegExp函数调用后返回了checkPhone和checkEmail函数。
curringAddEvent函数调用后返回了addEvent函数。
这些返回的函数并不会立即执行,而是等待后续调用。
3 封装通用柯里化工具函数
在前面的例子中,我们对函数进行柯里化都是手动修改原函数,将add改为curryingAdd、将checkByRegExp改为curryingCheckByRegExp、将addEvent改为curryingAddEvent。
难道每次对函数进行柯里化都要手动修改底层函数吗?当然不是。
我们可以封装一个通用的柯里化工具函数(这也是面试中常见的手写代码题)。
/**
* @description: 将函数柯里化的工具函数
* @param {Function} fn 待柯里化的函数
* @param {array} args 已经接收的参数列表
* @return {Function}
*/
const currying = function(fn, ...args) {
// fn需要的参数个数
const len = fn.length
// 返回一个函数接收剩余参数
return function (...params) {
// 拼接已经接收和新接收的参数列表
let _args = [...args, ...params]
// 如果已经接收的参数个数还不够,继续返回一个新函数接收剩余参数
if (_args.length < len) {
return currying.call(this, fn, ..._args)
}
// 参数全部接收完调用原函数
return fn.apply(this, _args)
}
}
这个柯里化工具函数用于接收部分参数,然后返回一个新函数以等待接收剩余参数。通过递归直到接收到全部所需参数后,再通过apply调用原函数。
现在,我们基本无需手动修改原函数即可实现函数柯里化。
// 直接用工具函数返回校验手机、邮箱的函数
const checkPhone = currying(checkByRegExp(/^1\d{10}$/))
const checkEmail = currying(checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/))
但是,前述事件监听的例子无法直接使用此工具函数进行柯里化。原因在于前文所述,因为其条件直接从全局获取,情况较为特殊。若将条件改为从外部传入,则可以使用工具函数柯里化。当然,这种情况下直接修改原函数更为直接且可读性更强。
4 总结和补充
- 柯里化突出体现了一种重要思想:降低适用范围,提高适用性。
- 柯里化的三个主要作用和特点包括:参数复用、提前返回、延迟执行。
- 柯里化是闭包的一个典型应用,利用闭包形成保存在内存中的作用域,将接收到的部分参数保存在该作用域中,供后续使用,并返回一个新函数以接收剩余参数。
到此这篇关于JavaScript函数柯里化的文章就介绍到这了,更多相关函数柯里化内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
