函数柯里化(Currying
)在JavaScript
中总感觉属于一种不温不火的存在,甚至有些开发者在提起柯里化时,竟然会有点生疏不懂。其实不然,对于它们的概念可能在日常开发中不太提到,但是它们的思想和用法,却在前端开发中经常借鉴和使用,它可以帮助我们写出更加优雅、灵活的代码。本文将介绍柯里化概念、实现原理和应用场景,希望对大家能有所帮助!
函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。
通过柯里化,我们可以将一个接受多个参数的函数转换为一个接受一个参数的函数序列。这意味着我们可以先传递一部分参数,然后传递剩余的参数,或者分别传递参数,以此灵活地处理函数的调用。
例如,下面是一个接受两个参数的普通函数:
function add(a, b) {
return a + b
}
通过柯里化,我们可以将上述函数转换为一系列多个函数的调用:
function add(x) {
return function(y) {
return x + y;
}
}
进行调用:
add(1, 2); // 3
add(1)(2) // 3
通过柯里化,我们可以轻松地创建具有更高可复用性和灵活性的函数。它在函数式编程中经常被使用,并且可以用于创建高阶函数和函数组合。
函数柯里化的实现原理是利用闭包和递归。
具体步骤如下:
创建一个高阶函数,用于接受原函数的参数,并返回一个新函数。
在新函数内部,使用闭包来保存已经传入的参数。
在新函数内部,使用递归或者循环,判断是否所有参数都已经传入。若是,则执行原函数,并返回结果;若否,则继续返回新函数,接受下一个参数。
通过这样的方式,就可以实现柯里化函数。
以下是一个简单的示例代码展示柯里化的实现:
function currying(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 原函数
function add(x, y, z) {
return x + y + z;
}
// 柯里化后的函数
const curriedAdd = currying(add);
// 柯里化函数的调用
curriedAdd(1)(2)(3); // 返回 6
// 也可以一次传入多个参数
curriedAdd(1, 2)(3); // 返回 6
在上述代码中,currying 函数是一个高阶函数,用于接受原函数并返回柯里化后的函数。curried 函数是柯里化后的函数,在每次调用时判断传入的参数数量是否满足执行原函数的条件。
通过递归调用,每次返回一个新的函数,直到传入的参数数量满足原函数的要求,然后执行原函数并返回结果。这样就实现了函数的柯里化。
柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。 而这里对于函数参数的自由处理,正是柯里化的核心所在。 柯里化本质上是降低通用性,提高适用性。
我们工作中会遇到各种需要通过正则检验的需求,比如校验电话号码、校验邮箱、校验身份证号、校验密码等, 这时我们会封装一个通用函数 checkByRegExp
,接收两个参数,校验的正则对象和待校验的字符串。
// 函数封装后
function checkByRegExp(regExp,string) {
return regExp.test(string);
}
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校验电话号码
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); // 校验邮箱
上面这段代码,乍一看没什么问题,可以满足我们所有通过正则检验的需求。 但是我们考虑这样一个问题,如果我们需要校验多个电话号码或者校验多个邮箱呢?
我们可能会这样做:
checkByRegExp(/^1\d{10}$/, '18642838455'); // 校验电话号码
checkByRegExp(/^1\d{10}$/, '13109840560'); // 校验电话号码
checkByRegExp(/^1\d{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 = currying(checkByRegExp);
//生成工具函数,验证电话号码
let checkCellPhone = _check(/^1\d{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',
age: 20
},
{
name:'jack',
age: 23
}
]
我们需要获取数据中的所有 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'))
这么看来,通过柯里化的方式,我们的代码变得更精简了,并且可读性更高了。