本文深入解析JavaScript中的函数柯里化(curry)技术。从数学定义出发,探讨其在JS中的实际应用差异,并通过正则校验、数据提取等案例展示柯里化在参数复用和代码简化方面的核心价值。最后详细讲解如何封装支持占位符的通用curry工具函数,帮助开发者掌握这一高阶编程技巧。

一、什么是柯里化( curry)
在数学和计算机科学领域,柯里化(Currying)是一种将接收多个参数的函数转换为一系列只接收单个参数的函数的技术。
以一个接收3个参数的普通函数为例,经过柯里化处理后,新函数首先接收一个参数,并返回一个接收下一个参数的函数;该返回的函数再返回一个接收第三个参数的函数。当最后一个函数接收到第三个参数时,它会利用之前累积的所有参数调用原始函数,并返回最终的计算结果。
数学和计算科学中的柯里化:
// 数学和计算科学中的柯里化:
//一个接收三个参数的普通函数
function sum(a,b,c) {
console.log(a+b+c)
}
//用于将普通函数转化为柯里化版本的工具函数
function curry(fn) {
//...内部实现省略,返回一个新函数
}
//获取一个柯里化后的函数
let _sum = curry(sum);
//返回一个接收第二个参数的函数
let A = _sum(1);
//返回一个接收第三个参数的函数
let B = A(2);
//接收到最后一个参数,将之前所有的参数应用到原函数中,并运行
B(3) // print : 6
然而,在Javascript语言中,我们通常讨论的柯里化概念,与数学和计算机科学中的严格定义并不完全一致。
在严格的数学和计算机科学定义中,柯里化函数每次只能传递一个参数;
而在我们Javascript的实际开发应用中,柯里化函数允许一次性传递一个或多个参数。
来看这个例子:
//普通函数
function fn(a,b,c,d,e) {
console.log(a,b,c,d,e)
}
//生成的柯里化函数
let _fn = curry(fn);
_fn(1,2,3,4,5); // print: 1,2,3,4,5
_fn(1)(2)(3,4,5); // print: 1,2,3,4,5
_fn(1,2)(3,4)(5); // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
对于一个已经柯里化后的 _fn 函数而言,如果接收到的参数数量与原函数的形参数量相同,则直接执行原函数;如果接收到的参数数量少于原函数的形参数量,则返回一个新函数用于接收剩余参数,直到累积的参数数量与形参数量一致时,才执行原函数。
当我们了解了柯里化的定义后,接下来探讨柯里化在实际开发中的具体用途。
二、柯里化的用途
柯里化在某种程度上将简单问题复杂化了,但这种复杂化赋予了函数在使用时更高的自由度。对函数参数的灵活处理正是柯里化的核心所在。从本质上看,柯里化通过降低函数的通用性,换取了特定场景下适用性的提升。来看一个具体例子:
在开发工作中,我们经常遇到各种基于正则表达式的校验需求,例如校验电话号码、邮箱、身份证号或密码等。此时,我们通常会封装一个通用函数 checkByRegExp ,它接收两个参数:用于校验的正则对象和待校验的字符串。
function checkByRegExp(regExp,string) {
return regExp.test(string);
}
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校验电话号码
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校验邮箱
上述代码乍看之下没有问题,能够满足基本的正则校验需求。但我们需考虑这样一个场景:如果需要校验多个电话号码或多个邮箱,该如何处理?
我们可能会采取如下做法:
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校验电话号码
checkByRegExp(/^1\d{10}$/, '13109840560'); // 校验电话号码
checkByRegExp(/^1\d{10}$/, '13204061212'); // 校验电话号码
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校验邮箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@qq.com'); // 校验邮箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@gmail.com'); // 校验邮箱
每次进行校验时都需要输入一串正则表达式。在校验同一类型数据时,相同的正则表达式需要重复编写多次。这导致使用效率低下,且由于 checkByRegExp 函数本身只是一个无具体业务意义的工具函数,一段时间后重新审视这些代码时,若无注释说明,我们必须通过检查正则表达式的具体内容,才能判断其校验的是电话号码、邮箱还是其他类型。
此时,我们可以借助柯里化对 checkByRegExp 函数进行封装,从而简化代码书写并提高代码的可读性。
//进行柯里化
let _check = curry(checkByRegExp);
//生成工具函数,验证电话号码
let checkCellPhone = _check(/^1\d{10}$/);
//生成工具函数,验证邮箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
checkCellPhone('18642838455'); // 校验电话号码
checkCellPhone('13109840560'); // 校验电话号码
checkCellPhone('13204061212'); // 校验电话号码
checkEmail('test@163.com'); // 校验邮箱
checkEmail('test@qq.com'); // 校验邮箱
checkEmail('test@gmail.com'); // 校验邮箱
再来看看通过柯里化封装后的代码,是否变得更加简洁直观。
经过柯里化处理,我们生成了两个专用函数 checkCellPhone 和 checkEmail, checkCellPhone 函数仅能验证传入的字符串是否为电话号码, checkEmail 函数仅能验证传入的字符串是否为邮箱。与原函数 checkByRegExp 相比,它们在功能上的通用性虽然降低了,但适用性却得到了提升。这种柯里化的用途可以理解为:参数复用。
我们再来看另一个例子。
假定我们有如下数据结构:
let list = [
{
name:'lucy'
},
{
name:'jack'
}
]
我们需要获取数据中所有 name 属性的值。在常规思路下,我们会这样实现:
let names = list.map(function(item) {
return item.name;
})
那么,如何用柯里化的思维来实现这一需求呢?
let prop = curry(function(key,obj) {
return obj[key];
})
let names = list.map(prop('name'))
看到这里,读者可能会有疑问:如此简单的例子,仅仅为了获取 name 的属性值,为何还要专门实现一个 prop 函数?这似乎过于繁琐。
我们可以转换思路:prop 函数只需实现一次,之后便可多次复用。因此,在评估代码复杂程度时,我们可以忽略 prop 函数的实现成本。
我们实际的代码可以简化为仅有一行 let names = list.map(prop('name'))
由此可见,通过柯里化的方式,我们的代码变得更加精简,且可读性更高。
三、如何封装柯里化工具函数
接下来,我们来思考如何实现 curry 函数。
回顾之前对柯里化的定义:接收部分参数,返回一个接收剩余参数的函数,当接收到足够参数后,执行原函数。
我们已经知道,当柯里化函数接收到足够参数时会执行原函数,那么如何确定何时达到了“足够”的标准呢?
我们有两种实现思路:
- 通过函数的 length 属性获取函数的形参个数,形参的个数即为所需的参数个数。
- 在调用柯里化工具函数时,手动指定所需的参数个数。
我们将这两点结合,实现一个简单 curry 函数:
/**
* 将函数柯里化
* @param fn 待柯里化的原函数
* @param len 所需的参数个数,默认为原函数的形参个数
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}
/**
* 中转函数
* @param fn 待柯里化的原函数
* @param len 所需的参数个数
* @param args 已接收的参数列表
*/
function _curry(fn,len,...args) {
return function (...params) {
let _args = [...args,...params];
if(_args.length >= len){
return fn.apply(this,_args);
}else{
return _curry.call(this,fn,len,..._args)
}
}
}
我们来验证一下:
let _fn = curry(function(a,b,c,d,e){
console.log(a,b,c,d,e)
});
_fn(1,2,3,4,5); // print: 1,2,3,4,5
_fn(1)(2)(3,4,5); // print: 1,2,3,4,5
_fn(1,2)(3,4)(5); // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常实用的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。
例如,我们传入一个占位符,本次调用传递的参数会跳过占位符,占位符所在的位置由下次调用的参数来填充,比如这样:
直接看一下官网的例子:

接下来我们来思考,如何实现占位符的功能。
对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,因此将 lodash 对象作为默认占位符来使用。
而我们自己实现的 curry 函数,本身并未挂载在任何对象上,因此将 curry 函数作为默认占位符。
使用占位符的目的是改变参数传递的顺序,因此在 curry 函数的实现中,每次都需要记录是否使用了占位符,并记录占位符所代表的参数位置。
直接上代码:
/**
* @param fn 待柯里化的函数
* @param length 需要的参数个数,默认为函数的形参个数
* @param holder 占位符,默认当前柯里化函数
* @return {Function} 柯里化后的函数
*/
function curry(fn,length = fn.length,holder = curry){
return _curry.call(this,fn,length,holder,[],[])
}
/**
* 中转函数
* @param fn 柯里化的原函数
* @param length 原函数需要的参数个数
* @param holder 接收的占位符
* @param args 已接收的参数列表
* @param holders 已接收的占位符位置列表
* @return {Function} 继续柯里化的函数 或 最终结果
*/
function _curry(fn,length,holder,args,holders){
return function(..._args){
//将参数复制一份,避免多次操作同一函数导致参数混乱
let params = args.slice();
//将占位符位置列表复制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循环入参,追加参数 或 替换占位符
_args.forEach((arg,i)=>{
//真实参数 之前存在占位符 将占位符替换为真实参数
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index),1);
params[index] = arg;
}
//真实参数 之前不存在占位符 将参数追加到参数列表中
else if(arg !== holder && !holders.length){
params.push(arg);
}
//传入的是占位符,之前不存在占位符 记录占位符的位置
else if(arg === holder && !holders.length){
params.push(arg);
_holders.push(params.length - 1);
}
//传入的是占位符,之前存在占位符 删除原占位符位置
else if(arg === holder && holders.length){
holders.shift();
}
});
// params 中前 length 条记录中不包含占位符,执行函数
if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
return fn.apply(this,params);
}else{
return _curry.call(this,fn,length,holder,params,_holders)
}
}
}
验证一下:
let fn = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
}
let _ = {}; // 定义占位符
let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符
_fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5
我们已经完整实现了一个 curry 函数~~
到此这篇关于前端JavaScript彻底弄懂函数柯里化的文章就介绍到这了,更多相关JavaScript函数柯里化内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!