深度剖析Javascript的柯里化机制

385

这是我参与 8 月更文挑战的第 30 天,活动详情查看: 8月更文挑战

前言

柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。

从一道面试题说起

我们来看下这道面试题:

    sum(2)(3) // 5

要求该函数的返回结果为5,简单,我们来实现下代码:

    function sum(a) {
        return function(b) {
            return a + b
        }
    }

完美实现,这时候,题目需要加难度了,变成了这样

    sum(1)(2, 3)(4)() // 10

审题后发现,我们需要做以下几件事:

  1. 多个参数,需要相加,如sum(2, 3)等价于sum(5)
  2. 调用次数是不确定的,不能像上面一样写死返回函数
  3. 调用的参数为空时,需要返回结果

代码如下:

    function currying(fn){
        var allArgs = [];

        return function next(){
            var args = [].slice.call(arguments);

            if(args.length > 0){
                allArgs = allArgs.concat(args);
                return next;
            }else{
                return fn.apply(null, allArgs);
            }
        } 
    }

    var sum = currying(function(){
        var _sum = 0;
        for(var i = 0; i < arguments.length; i++){
            _sum += arguments[i];
        }
        return _sum;
    });

由于是延迟计算结果,所以要对参数进行记忆。

这里的实现方式是采用闭包。

function currying(fn){
    var allArgs = [];

    return function next(){
        var args = [].slice.call(arguments);

        if(args.length > 0){
            allArgs = allArgs.concat(args);
            return next;
        }
    } 
}

当执行var add = currying(...)时,add变量已经指向了next方法。此时,allArgs在next方法内部有引用到,所以不能被GC回收。也就是说,allArgs在该赋值语句执行后,一直存在,形成了闭包。

依靠这个特性,只要把接收的参数,不断放入allArgs变量进行存储即可。

所以,当arguments.length > 0时,就可以将接收的新参数,放到allArgs中。

最后返回next函数指针,形成链式调用。

另外,由于计算结果的方法,是作为参数传入currying函数,所以要利用apply进行执行。

综合上述思考,就可以得到以下完整的柯里化函数。

function currying(fn){
    var allArgs = []; // 用来接收参数

    return function next(){
        var args = [].slice.call(arguments);

        // 判断是否执行计算
        if(args.length > 0){
            allArgs = allArgs.concat(args); // 收集传入的参数,进行缓存
            return next;
        }else{
            return fn.apply(null, allArgs); // 符合执行条件,执行计算
        }
    } 
}

小结

最后,我们再来总结一下柯里化机制:

  1. 逐步接收参数,形式包含多个参数、多次调用,并缓存供后期计算使用
  2. 最后发出计算指令后,计算返回