我为什么要用柯里化?
在说为什么之前,首先把柯里化再解释下
百度词条解释为:
在计算机科学中,柯里化是把接受多个参数的函数转换为接受单一参数的函数,并返回接受余下参数且返回结果的新函数的技术。这个技术由 Christopher Strachey以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege发明的。
文字看起来可能有点绕,用代码说明就是:
const add = (a,b)=>{
return a+b
}
//手动柯里化
const curry = (fn)=>{
return (b)=>(a)=>{
return add(a,b)
}
}
//原函数
add(1,2)//3
//柯里化处理后
const curryAdd = curry(add)
const a = curryAdd(1)
const b = a(2) //3
从上面的使用情况,可以很直观的看出来,柯里化进行了参数拆分,但是这样做有什么好处呢
脱离业务来讨论技术大多无意义,我们结合下面的例子一起看下。
在前端项目中,接口请求的封装应该都不陌生。
//request为axios的request请求,做了一些请求拦截处理
为了方便调用,我们简单封装了post和get两种请求方式
//defaultPrefix 是请求的服务名,比如student-web服务,是请求url的一部分
const defaultPrefix = 'XXXX'
export function postRequest(url,data={},prefix){
return request({
url: `${prefix || defaultPrefix}/${url}`,
method: "POST",
data
})
}
export function getRequest(url,data,prefix){
return request({
url: `${prefix || defaultPrefix}/${url}`,
method: "GET",
params:data
})
}
//分别调用
postRequest('xxx/xxx',{})
getRequest('xxx/xxx',{})
然后你发现这两种方法重复逻辑很多,区别仅仅在于请求方式和传参,于是做了一个整合处理
export function postRequest(method='post',url,data={},prefix){
const defaultSet = {
url: `${prefix || defaultPrefix}/${url}`,
method
}
method.toLowerCase() == 'get' ? defaultSet.params = data : defaultSet.data = data
return request({
...defaultSet
})
}
//调用
postRequest('post','xxx/xxx',{})
随着业务开展,又有返回格式,请求参数头的需求增加,最后请求方式封装成如下:
/*
method:请求方式
options:扩展参数
prefix:请求前缀,默认fontPrefix
url:请求url部分
data:传参
*/
export function postRequest(method,url,options,data={},prefix){
const headers = data.headers ? data.headers : {}
data.headers ? delete data.headers :''
const defaultSet = {
url: `${prefix || defaultPrefix}/${url}`,
method,
headers:{
...headers
},
...options
}
method.toLowerCase() == 'get' ? defaultSet.params = data : defaultSet.data = data
return request({
...defaultSet
})
}
//调用一个post请求,该请求返回内容是二进制blob格式
postRequest('post','xxx/xxx/xxx',{
responseType:"blob"
},{},'')
上面的方法封装完了之后,可以在不同的业务中调用。
但是每次调用都要传好几个参数,而很多参数都是基本固定的,但是也会有少部分请求,参数都不太一样,本着’能懒则懒’的开发原则,柯里化就派上了用场。
lodash工具库里有curry方法可以直接用
import curry from 'lodash/fp/curry'
为了方便理解,我们自己写个简单版的
const defaultPrefix = 'XXXX'
const curry = (fn)=>{
const fnLen = fn.length//传入的fn的参数个数
const innerFun = (...params)=>{
//如果传入的参数个数少于原函数的参数个数,则返回当前函数,并接受余下的参数作为当前函数的参数
if(params.length <fnLen){
return innerFun(...params,...selfParam)
}else{
//如果传入的参数个数等于原函数的参数个数,执行原函数
return fn(...params)
}
}
return innerFun
}
const coreRequest = curry((method,options,prefix,url,data)=>{
const dataSet = {}
method.toLowerCase() == 'get' ? dataSet.params = data : dataSet.data = data
return request({
url:prefix+url,
method,
...dataSet,
...options
})
})
//常规json格式请求
const jsonContentType = {
headers:{
'Content-Type':'application/json;charset=utf-8'
}
}
//返回带第一个method参数的函数
const post = coreRequest('post')
const get = coreRequest('get')
//返回请求头部是application/json的post请求函数
const postJson = post(jsonContentType)
返回请求头部是application/json的get请求函数
const getJson = get(jsonContentType)
//返回带默认请求头的post请求
const postApi = postJson(defaultPrefix)
//返回带默认请求头的get请求
const getApi = postJson(defaultPrefix)
//业务调用地方传入url参数
const requestFun = postApi('xxxxxx')
//传入data参数
requestFun({
name:1
})
如果需要修改请求url的服务,改为teacher-web,可以重新定义一个函数如下:
const postDefineApi = postJson('teacher-web')
requestFun({
name:1
})
如果还有别的请求接口需求,可以根据coreRequest做不同的扩展返回不同的函数
上面还有个问题,如果requestFun不传参数(实际情况我们的接口请求,body数据或者param是有可能空的),那么curry方法不会走到最后一步,为了兼容这种情况,可以将curry做如下修改
const curry = (fn)=>{
const fnLen = fn.length//传入的fn的参数个数
const innerFun = (...params)=>{
if(params.length <fnLen){
//如果传入的参数个数少于原函数的参数个数,
//且后面还有参数则返回当前函数,并接受余下的参数作为当前函数的参数,否则执行函数
return (...selfParam)=>{
if(!selfParam || selfParam.length == 0){
return fn(...params)
}else{
return innerFun(...params,...selfParam)
}
}
}else{
//如果传入的参数个数等于原函数的参数个数,执行原函数
return fn(...params)
}
}
return innerFun
}
这样就兼顾了减少代码量和扩展性
总结下来:
个人认为柯里化有如下优缺点:
- 优点:节省重复传参,函数更灵活,扩展性高
- 缺点:如果参数多,返回的接受函数较多,看上去有点绕,理解上没有一个函数直观。且因为是闭包,可能有内存泄漏风险
当然是否采用柯里化函数,结合实际业务需求来即可。