柯里化是函数式编程的两个最基本的运算之一
柯里化
要想让一个 f(x) 和一个 g(x) 合成为 f(g(x)),有一个隐藏的前提,就是 f 和 g 都只能接受一个参数。如果可以接受多个参数,比如 f(x, y) 和 g(a, b, c),函数合成就非常麻烦。
这时就需要函数柯里化了。所谓”柯里化”,就是把一个多参数的函数,转化为单参数函数。
例子:
// 柯里化之前function add(x, y) {return x + y;}add(1, 2) // 3// 柯里化之后function addX(y) {return function (x) {return x + y;};}addX(2)(1) // 3//简化 addX(y)addX(y) = x => y =>x+y
有了柯里化以后,我们就能做到,所有函数只接受一个参数。后文的内容除非另有说明,都默认函数只有一个参数,就是所要处理的那个值。
如果我们想实现任意柯里化如:
function sum(a,b,c,d){return a+b+c+d}let sum2 = curry(sum)let sum3 = curry(sum)sum2(1)(2)(3,4) //10sum3(1)(2,3)(4) //10
curry分析
那我们要如何实现这个柯里化(curry)呢,先来分析一下,以 sum2 为例子。
首先 sum2 是一个函数,经过 curry 处理之后,得到另外一个结果,这个结果后面可以加括号,表示这个东西也是一个函数。这也是curry的雏形。
function curry(fn){return function(){return fn()}return function}}
接着往下分析,它接受一个函数,后面还能加括号,那就是它会返回另外一个函数,那么在这个例子中 sum2 = function,
如果我们的参数很多,是不是要嵌套很多很多层?涉及到这种不知道要嵌套多少层,不知道有多深的,我们就可以使用递归。
curry实现代码如下:
function curry(fn){let arr = []return function curried(...args){//...表示展开数组arr = [...arr,...args]if(arr.length >= fn.length){ //fn.length 即需要的参数的数量return fn(...arr)}return curried}}
理解递归最好的办法就是代入法,尝试着带入sum2 = function,并逐个传入参数。
//开始sum2 = function curried(...args){arr = [...arr,...args]if(arr.length >= fn.length){return fn(...arr)}return curried}
递归过程
//以sum2(1)(2)(3,4) 为例arr = [] //是一个空数组sum2(1) //此时,args = [1],arr=[1]arr.length<fn.lengthreturn curriedsum2(1)(2) //此时,args = [2],arr = [1,2]arr.length<fn.lengthreturn curriedsum2(1)(2)(3,4)//此时,args=[3,4],arr = [1,2,3,4]arr.length >= fn.lengthreturn fn(...arr) //递归结束,返回sum(1,2,3,4)
代码实践
既然 curry 已经写出来了,我们就检验一下它是否能够成功达到目的。
function sum(a,b,c,d){return a+b+c+d}function curry(fn){let arr = []return function curried(...args){//...表示展开数组arr = [...arr,...args]if(arr.length >= fn.length){ //fn.length 即需要的参数的数量return fn(...arr)}return curried}}let sum2 = curry(sum)sum2(1)(2)(3,4) //10

但是调试的时候马上会发现一个 bug,我如果想再次使用 sum2 算一个别的值呢?
sum2(4)(3)(9,6)

浏览器马上就会报错,说sum2不是一个函数,我不是明明已经声明了吗???
再次声明一边sum2。
let sum2 = curry(sum)sum2(4)(3)(9,6)
这一次就成功了
再来一次
实例证明,在这段代码中,每一次使用 sum2 时,我们都必须再次声明,那不是太蠢了?我们再想想,如果 sum2 不是一个函数了,那么它到底是什么呢?
为了结果更直观,在每一次递归的时候打印出 arr 的值。
function curry(fn){let arr = []return function curried(...args){//...表示展开数组arr = [...arr,...args]console.log(arr) //打印数组的值if(arr.length >= fn.length){ //fn.length 即需要的参数的数量return fn(...arr)}return curried}}
再次调用 sum2
可以发现,如果不重新声明的话,新的参数会加在原有的数组里,且第一个括号内有什么,数组里就会增加什么。
回顾代码,由于 arr.length 已经大于 fn.length,所以函数都会直接 return fn(…arr),而不会再次 return curried。所以,如果不再次声明 sum2,继续调用 sum2(),例如 sum2(1),得到的结果就会是一个 fn 函数的调用,也就是sum 函数之前计算出来的一个 Number 。
修改代码
function sum(a,b,c,d){return a+b+c+d}function curry(fn){let arr = []return function curried(...args){//...表示展开数组arr = [...arr,...args]console.log(arr)if(arr.length >= fn.length){ //fn.length 即需要的参数的数量let result = fn(...arr) //用另一个变量保存sum的值arr.length = 0 //初始化数组return result}return curried}}let sum2 = curry(sum)
修改之后就可以正常使用了。
总结
- 所谓”柯里化”,就是把一个多参数的函数,拆解转化为单参数函数。
- curry的核心原理:闭包。不断引用一开始保存参数的列表。
- curry 的递归思想就是判断是否符合参数数量,不符合就继续 return curried。
