前言:
最近在看Taro文档,里面提到了js函数的柯里化,完全不懂。搜了写资料后,大概理解了点意思,记录一下心得
参考:前端基础进阶(八):深入详解函数的柯里化
1. 作用:
将传统多参函数转换为链式函数调用,方便对函数进行拆分封装。
2.核心:
利用js闭包捕获每次链式调用的参数并储存,最后一次调用时,对原函数进行apply调用
3.举例:
假设有个工厂(函数)可以放入样板,原材料(参数),生产出成品衣服(结果)。传统函数将样板,材料a,材料b均作为参数传给工厂,生成产品。柯里化后,该工厂可以先接收样板,升级为新的工厂(衣服工厂,裤子工厂),接着再将材料传给该工厂后,最终输出产品。在传输完材料前,工厂返回的就是含有部分材料的新工厂(中间函数),只有全部材料全部传递完成或者下达生产命令(变参函数)后,才能返回产品。
思考题:
实现add()方法,使得以下调用形式:
add(1,2,3,4)
add(1,2)(3,4)
add(1,2,3)(4)
add(1)(2)(3)(4)
均输出结果10
简化思考:
普通相加函数:
function nomalAdd(...params)
{
return params.reduce((a,b) => a+b)
}
简单链式:
function adder(...firstParam)
{
var result = nomalAdd(...firstParam)
return function(...secondParams)
{
result += nomalAdd(...secondParams)
return result
}
}
var result = adder(1,2)(3,4)
console.log(result) //10
可以看到,简单链式函数 其实就是两个嵌套函数,外层函数adder()返回的是一个函数,并通过闭包的特性,将第一次计算的结果传递给闭包储存起来,第二次调用时候就能得出正确结果。这就是最简单的柯里化思想,每次链式调用,均返回内部闭包函数,并将上一步结果储存,最后返回正确结果,如果每次储存的不是上一步的结果,而是将每一次调用的参数储存起来,最终将全部参数应用于原始函数,这就是完整的柯里化思维。
函数的柯里化
对于传统多参数函数,通过某个方法将其转换为可以链式调用的函数,称为函数的柯里化,本质上就是上面所说。缺点是刚开始看不太容易理解,很容易疑惑为什么折腾一圈后,最终还是使用了原函数通过apply传入原始参数来调用。优点是可以将函数拆分,方便进行二次封装。
1.定参函数(函数参数个数固定):
function add(a,b,c)
{
return a+b+c
}
//封装的柯里化函数
function createCurry(func)
{
var funcParamCount = func.length//函数的length返回函数参数个数
var savedArgs = []//用来储存函数
var innerFunc = function(...newArgs)//返回的闭包函数,用来链式调用
{
savedArgs.push(...newArgs)//将新的链式调用的参数加入到保存函数的数组中
if (savedArgs.length == funcParamCount)//如果储存的数组个数等于原始函数的参数个数,说明链式调用结束
{
return func.apply(this, savedArgs)//使用apply,将保存的所有函数传递给原始函数调用,返回结果
}
else
{
return innerFunc//否则说明链式调用未结束,将闭包函数自身返回回去
}
}
return innerFunc//柯里化时,生成的就是该闭包函数
}
//使用
var adder = createCurry(add)//柯里化生成adder函数
var res = adder(1)(2)(3)//链式调用
console.log(res) //6
var midderAdder1 = adder(1)//调用完成前,中间值均为adder闭包函数
var midderAdder2 = adder(1)(2)
console.log(typeof(midderAdder))//function
console.log(midderAdder1 == midderAdder2)//true
2.不定参函数(函数参数个数不确定):
function add(...args) {
return args.reduce((a,b) =>
{
return a+b
})
}
//封装的柯里化函数
function createCurry(func)
{
var funcParamLength = func.length//依然记录原始函数的个数,不定参函数时为0
var savedArgs = []
var innerFunc = (...newArgs) => {
if (newArgs.length > 0)
{
savedArgs.push(...newArgs)
}
if (funcParamLength > 0)//定参函数
{
if (savedArgs.length < funcParamLength)
{
return innerFunc
}
else
{
var result = func.apply(this, savedArgs)
savedArgs = []//清空保存的参数
return result
}
}
else//不定参函数
{
if (newArgs.length > 0)//链式调用在继续
{
return innerFunc
}
else//认为链式调用结束
{
var result = func.apply(this, savedArgs)
savedArgs = []
return result
}
}
}
return innerFunc
}
//调用
var adder = createCurry(add)
var result = adder(1,2)(4,5)
console.log(typeof(result)) //function
console.log(adder == result)//true
console.log(result()) //12
因为不定参函数需要确定在何时停止链式调用,返回结果,所以默认为不传入参数调用时,链式调用结束。同时这也是上面思考题的解法,柯里化后的adder函数,就是答案:
var adder = createCurry(add)
console.log(adder(1,2,3,4)()) //10
var adder = createCurry(add)
console.log(adder(1,2)(3,4)()) //10
var adder = createCurry(add)
console.log(adder(1,2,3)(4)()) //10
var adder = createCurry(add)
console.log(adder(1)(2)(3)(4)())//10
有缺陷在于,链式调用之后需要在调用一次空参数才能输出正确结果,否则则输出function,这是因为链式调用的中间值为函数对象,可以给函数对象添加toString()方法,即可在打印时,作为number类型输出:
var aFunc = function () {
console.log('I am a function')
}
aFunc.toString = function() {
return "I am a function description"
}
console.log(aFunc) //I am a function description
aFunc() //I am a function
console.log(typeof(aFunc)) //function
因此稍微修改createCurry()方法
//修改createCurry
function createCurry(func)
{
var funcParamLength = func.length//依然记录原始函数的个数,不定参函数时为0
var savedArgs = []
var innerFunc = (...newArgs) => {
if (newArgs.length > 0)
{
savedArgs.push(...newArgs)
}
if (funcParamLength > 0)//定参函数
{
if (savedArgs.length < funcParamLength)
{
return innerFunc
}
else
{
var result = func.apply(this, savedArgs)
savedArgs = []//清空结果
return result
}
}
else//不定参函数
{
if (newArgs.length > 0)//链式调用在继续
{
return innerFunc
}
else//认为链式调用结束
{
var result = func.apply(this, savedArgs)
savedArgs = []
return result
}
}
}
//添加toString()方法,输出结果
innerFunc.toString = () => {
var result = func.apply(this, savedArgs)
savedArgs = []
return result
}
return innerFunc
}
调用:
var adder = createCurry(add)
var result = adder(1)(2)(3,4) //f 10
console.log(result + 10) //20
console.log(result + '10') //1010
以上,链式调用后,可输出结果,并且使用结果进行运算等操作时,会隐式转换为相应类型
不使用柯里化函数,直接写链式adder()方法:
function nomalAdder(...nums) {
var savedArgs = nums || [];//首次调用,储存参数或者初始化储存参数的数组
var innerFunc = (...newNums) => {//返回的链式闭包函数
if (newNums.length > 0)//还有参数调用,继续存
{
savedArgs.push(...newNums)
return innerFunc
}
else//参数为空时,停止链式调用
{
return savedArgs.reduce((a,b) => a+b)
}
}
innerFunc.toString = () => innerFunc()//输出时,停止链式调用
return innerFunc
}
调用
var result1 = nomalAdder(1,2,3,4) //f 10
var result2 = nomalAdder(1,2)(3,4) //f 10
var result3 = nomalAdder(1,2,3)(4) //f 10
var result4 = nomalAdder(1)(2)(3)(4)//f 10
console.log(result1 + result2 + result3 + result4)//40
柯里化本质其实就是通过返回一个闭包函数,并将每次链式调用的函数储存起来,最后统一调用原函数的过程。举例用处:
比如需要封装一个正则判断输入数据的函数:
function check(targetString, reg) {
return reg.test(targetString);
}
调用时:
check(/^1[34578]\d{9}$/, '14900000088');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
通过柯里化,我们可以先对check函数再次进行封装,升级工厂:
var _check = createCurry(check);
var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
这样使用时就会更加直观明了
checkPhone('183888888');
checkEmail('xxxxx@test.com');
以上就是个人对于柯里化的理解,因为不是太熟悉js,其中可能有理解错误。希望有路过的大神能帮忙指出来。谢谢~
另外这个思想理解起来的确有点绕,想要熟练使用,还得多练习,对于不同场景多想想如何封装代码。