JS 柯里化

1,327 阅读3分钟

1、什么是柯里化

柯里化(currying)是一种函数转化技术,它把一个接受多个参数的函数转化为可接受单个函数参数,并返回接受余下参数,最终函数返回与原函数相同结果的函数。 说人话就是:柯里化就是实现函数从f(a, b, c)到f(a)(b)(c)的转化

看一个简单的3数想加的函数add():

function add(a, b, c){
    return a + b + c;
}

它对应的柯里化函数curriedAdd():

function curriedAdd(a){
    return function(b){
        return function(c){
            return a + b + c;
        }
    }
}
//使用
curriedAdd(1)(2)(3);        //6
let add10 = curriedAdd(10);   //得到一个函数
add10(1)(2);                //13;

值得注意的是,柯里化运用了函数闭包的特性。

2、柯里化的意义

f(a)(b)(c)相对f(a, b, c)有什么优势?优点在于参数复用

考虑一个乘方函数pow(base, exponent),我们可以通过pow(x, y)来计算x的y次方。假设我们需要频繁计算2的幂。则会多次产生如pow(2, 3),pow(2,7), pow(2, 9)之类的调用,尽管第一个参数都是2,但你不得不每次调用都传递。如果使用柯里化的形式,可以有下面的使用方法。

let pow2 = curriedPow(2);         //pow2仍然是一个函数
pow2(3);  //相当于pow(2,3);
pow2(7);
pow2(9);

可以发现f(a)(b)(c)其实是产生了一系列的函数,我们可以方便得获取到函数的偏函数。

3、实现函数柯里化

如果对一个已经存在的函数,我们需要手动写出它的柯里化函数(如curriedAdd所示), 这代价甚至高于柯里化带来的便利。所以,有没有方法能实现自动对函数的柯里化?在JS这类函数式编程语言中,这是肯定的。看一个对开头add(a,b,c)柯里化的方法。

function currying(func){
    return function f(a){
        return function f(b){
            return function f(c){
                return func(a, b, c);
            };
        };
    };
}

function add(a, b, c){
    return a + b + c;
}

let curriedAdd = currying(add);
curriedAdd(1)(2)(3);           //6

currying实现了对任意的三参数函数的柯里化。为了摆脱函数参数数目的限制,我们考虑一个更通用的版本。

function curry(func){
    return function curried(...args){
        if(args.length>=func.length){
            return func.apply(this, args);
        }else{
            return function(...args2){
                return curried.apply(this, args.concat(args2));
            }
        }
    }
}

现在,你可以使用上面的curry()对任何函数进行柯里化。因为累计参数的原因,你不仅可以通过f(a)(b)(c)的方式调用,也可以用f(a,b)(c), f(a)(b, c), f(a, b, c)的形式调用。

function add(a, b, c, d){
    return a + b + c + d;
}

let curriedAdd = curry(add);
curriedAdd(1, 2, 3, 4);  //10
curriedAdd(2, 3)(4, 5);   //14
//延迟计算
let add7 = curriedAdd(7);
let add14 = add7(7);
let add21 = add14(7);
let result = add21(7);    //这里才计算结果。result = 28

你以为完了?其实上面遗漏了一类函数,就是形如func(...args)的不定参数的函数。首先,func.length === 0,导致上面的判断args.length>=func.length无效。其次,对不定参数,我们难以确定其何时计算。比如func(a)(b)(c)我们是该直接计算出它的值吗?还是得到一个偏函数?无论如何,我们需要确定何时计算结果。

一般的,对不定参数的函数,规定在无参数调用时返回结果,即func(a)(), func(a)(b)(), func(a)(b)(c)()这类计算结果。这样一来,就解决了上面的问题,我们把判断条件定为参数是否为空,并且在参数为空时返回结果,否则返回一个偏函数。

//不定参数函数的柯里化
function curry1(func){
    let argArr = []
    return function temp(...args){
        if(!args.length){
            let result = func.apply(this,argArr);
            argArr = [];    //保证下次调用
            return result;
        }else{
            argArr = argArr.concat(args);
            return temp;
        }
    }
}

同样,测试一下使用:

function add1(...args) {
    return args.reduce((a, b) => a + b);
}
let cAdd1 = curry1(add1);
console.log(cAdd1(1)(2)(3)(4, 5)());   //15

可以看到,确实如愿工作了。如果想用一个函数实现确定参数与不定参数的函数都能转化,在最外层套一个判断就行了,就不赘述了。

感谢!