柯里化与反柯里化

前言

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

反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,能够接收更多参数。目的是建立一个更普适性的函数,能够被不一样的对象使用。有鸠占鹊巢的效果。java

1、柯里化

1.1 例子

实现 add(1)(2, 3)(4)() = 10 的效果node

依题意,有两个关键点要注意:缓存

  • 传入参数时,代码不执行输出结果,而是先记忆起来
  • 当传入空的参数时,表明能够进行真正的运算

完整代码以下:闭包

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 add = currying(function(){
    var sum = 0;
    for(var i = 0; i < arguments.length; i++){
        sum += arguments[i];
    }
    return sum;
});

1.2 记忆传入参数

因为是延迟计算结果,因此要对参数进行记忆。
这里的实现方式是采用闭包。app

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方法。此时,allArgsnext方法内部有引用到,因此不能被GC回收。也就是说,allArgs在该赋值语句执行后,一直存在,造成了闭包。
依靠这个特性,只要把接收的参数,不断放入allArgs变量进行存储便可。
因此,当arguments.length > 0时,就能够将接收的新参数,放到allArgs中。
最后返回next函数指针,造成链式调用。函数

1.3 判断触发函数执行条件

题意是,空参数时,输出结果。因此,只要判断arguments.length == 0便可执行。
另外,因为计算结果的方法,是做为参数传入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.4 总结

柯里化,在这个例子中能够看出很明显的行为规范:this

  • 逐步接收参数,并缓存供后期计算使用
  • 不当即计算,延后执行
  • 符合计算的条件,将缓存的参数,统一传递给执行方法

1.5 扩展

实现 add(1)(2, 3)(4)(5) = 15 的效果。
不少人这里就犯嘀咕了:我怎么知道执行的时机?
其实,这里有个忍者技艺:valueOftoString
js在获取当前变量值的时候,会根据语境,隐式调用valueOftoString方法进行获取须要的值。
那么,实现起来就很简单了。prototype

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

    function next(){
        var args = [].slice.call(arguments);
        allArgs = allArgs.concat(args);
        return next;
    }
    // 字符类型
    next.toString = function(){
        return fn.apply(null, allArgs);
    };
    // 数值类型
    next.valueOf = function(){
        return fn.apply(null, allArgs);
    }

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

2、反柯里化

2.1 例子

有如下轻提示类。如今想要单独使用其show方法,输出新对象obj中的内容。

// 轻提示
function Toast(option){
  this.prompt = '';
}
Toast.prototype = {
  constructor: Toast,
  // 输出提示
  show: function(){
    console.log(this.prompt);
  }
};

// 新对象
var obj = {
    prompt: '新对象'
};

用反柯里化的方式,能够这么作

function unCurrying(fn){
    return function(){
        var args = [].slice.call(arguments);
        var that = args.shift();
        return fn.apply(that, args);
    }
}

var objShow = unCurrying(Toast.prototype.show);

objShow(obj); // 输出"新对象"

2.2 反柯里化的行为

  • 非我之物,为我所用
  • 增长被反柯里化方法接收的参数

在上面的例子中,Toast.prototype.show方法,原本是Toast类的私有方法。跟新对象obj没有半毛钱关系。
通过反柯里化后,却能够为obj对象所用。
为何能被obj所用,是由于内部将Toast.prototype.show的上下文从新定义为obj。也就是用apply改变了this指向。
而实现这一步骤的过程,就须要增长反柯里化后的objShow方法参数。

2.3 另外一种反柯里化的实现

Function.prototype.unCurrying = function(){
    var self = this;
    return function(){
        return Function.prototype.call.apply(self, arguments);
    }
}

// 使用
var objShow = Toast.prototype.show.unCurrying();
objShow(obj);

这里的难点,在于理解Function.prototype.call.apply(self, arguments);
能够分拆为两步:

1) Function.prototype.call.apply(...)的解析

能够当作是callFunction.apply(...)。这样,就清晰不少。
callFunctionthis指针,被apply修改成self
而后执行callFunction -> callFunction(arguments)

2) callFunction(arguments)的解析

call方法,第一个参数,是用来指定this的。因此callFunction(arguments) -> callFunction(arguments[0], arguments[1-n])
由此能够得出,反柯里化后,第一个参数,是用来指定this指向的。

3)为何要用apply(self, arguments)
若是使用apply(null, arguments),由于null对象没有call方法,会报错。

3、实战

3.1 判断变量类型(反柯里化)

var fn = function(){};
var val = 1;

if(Object.prototype.toString.call(fn) == '[object Function]'){
    console.log(`${fn} is function.`);
}

if(Object.prototype.toString.call(val) == '[object Number]'){
    console.log(`${val} is number.`);
}

上述代码,用反柯里化,能够这么写:

var fn = function(){};
var val = 1;
var toString = Object.prototype.toString.unCurrying();

if(toString(fn) == '[object Function]'){
    console.log(`${fn} is function.`);
}

if(toString(val) == '[object Number]'){
    console.log(`${val} is number.`);
}

3.2 监听事件(柯里化)

function nodeListen(node, eventName){
    return function(fn){
        node.addEventListener(eventName, function(){
            fn.apply(this, Array.prototype.slice.call(arguments));
        }, false);
    }
}

var bodyClickListen = nodeListen(document.body, 'click');
bodyClickListen(function(){
    console.log('first listen');
});

bodyClickListen(function(){
    console.log('second listen');
});

使用柯里化,优化监听DOM节点事件。addEventListener三个参数不用每次都写。

后记

其实,反柯里化和泛型方法同样,只是理念上有一些不一样而已。理解这种思惟便可。

相关文章
相关标签/搜索