函数柯里化
什么是函数柯里化
在计算机科学中,柯里化(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)
可以看到,变换后的的函数可以分批次接受参数,先记住这一点,下面会讲用处。甚至fn(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可以继续柯里化。就是一个套娃的过程~
柯里化的作用和特点
参数复用
工作中会遇到的需求:通过正则校验电话号、邮箱、身份证是否合法等等
于是我们会封装一个校验函数如下:
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)解决了兼容性问题,但也会带来使用的不便,比如不同的应用场景需要传递多个不同的参数来解决问题
有的时候同一种规则可能会反复使用(比如校验手机的参数),这就造成了代码的重复,利用柯里化就能够消除重复,达到复用参数的目的。
柯里化的一种重要思想:降低适用范围,提高适用性
提前返回
在JS DOM事件监听程序中,我们用addEventListener方法为元素添加事件处理程序,但是部分浏览器版本不支持此方法,我们会使用attachEvent方法来替代。
这时我们会写一个兼容各浏览器版本的代码:
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是经过判断后得到的函数,以后调用就不用重复判断了。
这就是提前返回或者说提前确认,函数柯里化后可以提前处理部分任务,返回一个函数处理其他任务
延迟执行
事实上,上述正则校验和事件监听的例子中已经体现了延迟执行。
curryingCheckByRegExp函数调用后返回了checkPhone和checkEmail函数
curringAddEvent函数调用后返回了addEvent函数
返回的函数都不会立即执行,而是等待调用。
封装通用柯里化工具函数
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, [...args, ...args2]);
}
}
};
}
用例:
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
console.log( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
console.log( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
console.log( curriedSum(1)(2)(3) ); // 6,全柯里化
总结
柯里化突出一种重要思想:降低适用范围,提高适用性
柯里化的三个作用和特点:参数复用、提前返回、延迟执行
柯里化是闭包的一个典型应用,利用闭包形成了一个保存在内存中的作用域,把接收到的部分参数保存在这个作用域中,等待后续使用。并且返回一个新函数接收剩余参数