彻底弄懂函式柯里化
前言随着主流JavaScript中函数语言程式设计的迅速发展, 函式柯里化在许多应用程序中已经变得很普遍。 了解它们是什么,它们如何工作以及如何充分利用它们非常重要。
什么是柯里化( curry)
在数学和电脑科学中,柯里化是一种将使用多个引数的一个函式转换成一系列使用一个引数的函式的技术。
举例来说,一个接收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.text(string);
}
checkByRegExp(/^1d{10}$/, \'18642838455\'); // 校验电话号码
checkByRegExp(/^(w)+(.w+)*@(w)+((.w+)+)$/, \'[email protected]\'); // 校验邮箱
复制程式码
上面这段程式码,乍一看没什么问题,可以满足我们所有通过正则检验的需求。 但是我们考虑这样一个问题,如果我们需要校验多个电话号码或者校验多个邮箱呢?
我们可能会这样做:
checkByRegExp(/^1d{10}$/, \'18642838455\'); // 校验电话号码
checkByRegExp(/^1d{10}$/, \'13109840560\'); // 校验电话号码
checkByRegExp(/^1d{10}$/, \'13204061212\'); // 校验电话号码
checkByRegExp(/^(w)+(.w+)*@(w)+((.w+)+)$/, \'[email protected]\'); // 校验邮箱
checkByRegExp(/^(w)+(.w+)*@(w)+((.w+)+)$/, \'[email protected]\'); // 校验邮箱
checkByRegExp(/^(w)+(.w+)*@(w)+((.w+)+)$/, \'[email protected]\'); // 校验邮箱
复制程式码
我们每次进行校验的时候都需要输入一串正则,再校验同一型别的资料时,相同的正则我们需要写多次, 这就导致我们在使用的时候效率低下,并且由于 checkByRegExp 函式本身是一个工具函式并没有任何意义, 一段时间后我们重新来看这些程式码时,如果没有注释,我们必须通过检查正则的内容, 我们才能知道我们校验的是电话号码还是邮箱,还是别的什么。
此时,我们可以借助柯里化对 checkByRegExp 函式进行封装,以简化程式码书写,提高程式码可读性。
//进行柯里化
let _check = curry(checkByRegExp);
//生成工具函式,验证电话号码
let checkCellPhone = _check(/^1d{10}$/);
//生成工具函式,验证邮箱
let checkEmail = _check(/^(w)+(.w+)*@(w)+((.w+)+)$/);
checkCellPhone(\'18642838455\'); // 校验电话号码
checkCellPhone(\'13109840560\'); // 校验电话号码
checkCellPhone(\'13204061212\'); // 校验电话号码
checkEmail(\'[email protected]\'); // 校验邮箱
checkEmail(\'[email protected]\'); // 校验邮箱
checkEmail(\'[email protected]\'); // 校验邮箱
复制程式码
再来看看通过柯里化封装后,我们的程式码是不是变得又简洁又直观了呢。
经过柯里化后,我们生成了两个函式 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 函式~~





























