1. 函数柯里化简介
**函数柯里化是指把接收多个参数的函数转换为接受单一参数的函数,并返回接收剩下参数的新函数的技术。**通俗点说,就是将多元函数转化为多个单元函数的连续定义(这里的元代指参数)。
也就说函数柯里化可以把f(a,b,c)这样的多参的函数转换成f(a)(b)©这样的函数,经过转换后的函数每次依次接收单一参数,并且返回最终结果。
柯里化其实本质上是一种编程思想,函数执行产生一个闭包,可以把一些信息存储下来,目的是供下级上下文调用。所以函数柯里化的核心其实是预先存储+调用。
举个例子:
// 一个一次性接受三个参数的普通函数
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
function curry(fn) {
// 函数柯里化会将add普通函数转换成一个新的函数fn,这个fn可以接受单个参数,并返回函数fn1
// 下面会介绍具体实现
}
// 经过函数柯里化转换之后的函数
const newAdd = curry(sum);
// 返回一个可以接收第二个参数的函数
const add1 = newAdd(1);
// 返回一个可以接收第三个参数的函数
const add2 = add1(2);
// 传入第三个参数,返回最终结果
// 本质是借用了js闭包的特性,内部保存了之前的值1和2,从而得到6
const result = add2(3) // 6
2. 应用场景
函数柯里化可以让我们在使用函数时对参数进行自由处理,降低了通用性,提升了适用性。
假设我们在日常使用ajax发起请求时。我们有一个通用的请求的getAjax
函数来发起请求。
function ajax(method, url, query) {
const xhr = new XMLHttpRequest();
xhr.open(method, `${url}?${query}`);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readystate === 4) {
// xxx
}
}
}
// 这几个请求之间存在相同的参数
ajax('POST', 'www.test.com', 'name=zs')
ajax('POST','www.test2.com', 'name=ls')
ajax('POST','www.test3.com', 'name=ww')
如果它们有共同的请求方式,仍旧需要进行三次相同的传参,可以使用函数柯里化将其通用参数保存在闭包里,实现复用。它们和原函数相比,从功能上来说,降低了通用型性,但提升了适用性,因为它进行了参数复用。
const newAjax = curry(ajax);
const getPostAjax = newAjax('POST');
// 可以直接复用之前的残暑
getPostAjax('www.test.com', 'name=zs')
getPostAjax('www.test2.com', 'name=ls')
getPostAjax('www.test3.com', 'name=ww')
再看一个例子:
假如我们有这样这样一段数据,obj
是一个对象数组,里面每一项都是一个对象
const obj = [
{
name: 'zs',
age: 20,
},
{
name: 'lisi',
age: 23,
}
]
如果此时需要获取对象里面每一项数据的`name属性值,一般我们会这样去做:
const nameList = obj.map(item => item['name']);
假如用函数柯里化去实现:
// 封装一个获取props参数的通用函数
function props = curry(function(key, obj) {
return obj[key];
})
// 通过通用函数获取属性
let nameList = obj.map(props('name'));
应用函数柯里化之后,该props函数以后可以被多次使用。
而且在考虑代码复杂度的时候,是可以将该props函数去掉的,该函数内可以理解为只有一句let nameList = obj.map(props('name'));
。这样看起来代码会更加精简,并且可读性看起来也变得更高。
3. 具体实现
函数柯里化的具体实现原理其实就是保存之前传入的参数,返回接收剩余函数的函数,并返回最终结果。函数柯里化大致可以分为两种:定参函数柯里化和不定参的函数柯里化。
以sum求和为例说明。
- 定参函数柯里化
// 普通的求和函数
function sum(a,b,c) {
return a+b+c
}
sum(1, 2, 3) // 6
// 经过柯里化转换后的函数
let newSum = curry(sum);
newSum(1)(2)(3) //6
// 柯里化的实现
function curry(fn, ...args) {
return function() {
// 利用js闭包的特性获取上下文函数的传参,做参数拼接
args = [...args, ...arguments]
// 如果当前传参的数量不够,就返回能够接收剩余参数的函数
// fn.length获取的是fn函数中的形参数量
if (args.length < fn.length) {
// 把当前参数作为函数形参进行传递
return curry.call(this, fn, ...args);
} else {
// 函数参数接收完成之后,执行函数
return fn.apply(this, args);
}
}
}
-
不定参数函数的柯里化
不定参数函数的柯里化也是利用了js闭包+函数隐形转换实现的。
首先内部定义一个函数curry,用curry去接收函数递归调用传递的参数和之前的参数做拼接并保存在函数内部。
判断函数结束这里其实是用到了函数的隐式转换,因为每次执行返回值是一个函数,这个时候toString方法会自动被调用。
// 不定参 function sum(...params) { // 使用闭包特性保存args变量 let args = []; let curry = function() { args = [...args, ...arguments]; // 返回curry,供后续调用 return curry; } // return curry其实就是获取curry函数源码 // 获取curry函数源码会自动调用toString方法,所以在这里我们重写toString方法做求和 curry.toString = function() { const res = args.reduce((pre, cur) => { return pre + cur }, 0); // toSting返回值必须是string类型,否则会报错,因此这里做了一些字符串转换 return res + ''; } // 利用curry函数拼接参数 // 这里需要使用rest运算符对参数再做一遍展开,因为在curry函数中rest运算符只是做了类数组对象到数组对象的转换,不会对实际数组做展开 return curry(...params); } sum(1)(2)(3) // 6
我们对上面部分代码做具体分析,首先分析下面代码中的return curry
和return curry()
内部到底做了什么事情?
let curry = function() {
args = [...args, ...arguments];
// 返回curry,供后续调用
return curry;
}
return curry(...params);
-
当
return curry()
的时候:会去执行curry
函数体,然后抛出返回值 -
当
return curry
时:这个时候不会去执行curry
函数体,而是试图得到curry
函数体的源码。curry
函数名在系统内部其实是指向函数的指针,它在是传递了函数体所在的内部地址位置,在需要使用时根据这个指针到内存中去查找。return curry
的curry
得到的就是函数体的源码,而要得到源码,就会自动调用toString方法。在Function需要转换为字符串时,通常会自动调用函数的toString方法,toString方法返回一个表示当前函数源代码的字符串。
函数柯里化其实利用闭包形成了一个保存在内存中的变量args,并且把后续接收到的参数都拼接在args变量中,等到后续使用,并返回一个函数接收新的参数。因此,函数柯里化 = 闭包 + 递归。
4. 特点
-
参数复用
在上面提到了
ajax
函数柯里化的例子中,它在内部对POST
参数进行了缓存,有效进行了参数复用,减少函数传参。 -
提前返回
在普通函数中,需要等到所有参数都传递之后才可以执行得到一个返回结果。在函数柯里化中,每次传参执行都可以得到一个结果,这里的结果是个函数,存在着上级闭包的引用
-
延迟执行
在有些参数不是那么容易获取时,比如某个函数的函数是一个异步结果时,那此时就可以体现函数柯里化的优势了。如果函数参数依赖于另一个异步任务,那么就可以延迟执行,直到获取到所有异步执行的结果之后再去执行。
5. 函数柯里化与偏函数
- 偏函数
函数柯里化的一个独特应用场景就是实现偏函数。偏函数是指固定一个参数的一些函数,然后返回另一个更小元(参数)的函数,这个更小元的函数用于接收剩余参数并返回结果。
举个🌰:
我们有一个计算长方体体积的函数getVolumn
,一般使用如下:
function getVolumn(a,b,c) {
return a*b*c
}
getVolumn(100, 200, 300)
getVolumn(100, 200, 400)
getVolumn(100, 200, 500)
因为上述计算中部分长方体的长、宽都固定,所以可以使用偏函数来实现。
function partial(fn, a, b) {
// 使用偏函数固定部分参数
// ...
}
// 生成getVolumn的偏函数getVolumn100,这个偏函数的预设值是长100,宽200
const getVolumn100 = partial(getVolumn, 100, 200)
// 使用偏函数接收剩余参数,并结合之前预设的参数得出结果。
getVolumn100(300)
getVolumn100(400)
getVolumn100(500)
-
偏函数的实际应用场景
bind函数是偏函数的一个实际应用场景。bind函数可以让我们传入一个或多个参数作为预设值,并返回一个函数。当该函数被调用时,预设值就会作为参数被插入到当前函数参数列表之前执行。
上述场景也可以使用bind实现
const getVolumn100 = getgetVolumn.bind(null, 100, 200);
-
偏函数的实现
function partial(fn, ...args) { // 返回接收剩余参数的函数 return function() { // 把预设值和当前传参做拼接 args = [...args, ...arguments]; return fn.apply(this, args); } }
-
函数柯里化和偏函数的区别
函数柯里化和偏函数很类似,本质都是用于将多元函数转换更少元函数的方法。但是还是有些细微区别:
- 函数柯里化是把一个多元函数转化成了多个一元函数,即fn(a,b,c) => fn(a)(b)©,每个函数只接收一个参数
- 偏函数是固定一个多元函数的一个或多个参数,返回接收剩余参数的函数。即fn(a, b,c) => fn1©,它固定了a,b参数作为预设值,并可以接收剩余的参数