Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

柯里化与反柯里化 #10

Open
wall-wxk opened this issue Jul 23, 2018 · 0 comments
Open

柯里化与反柯里化 #10

wall-wxk opened this issue Jul 23, 2018 · 0 comments

Comments

@wall-wxk
Copy link
Owner

wall-wxk commented Jul 23, 2018

前言

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

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

一、柯里化

1.1 例子

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

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

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

完整代码如下:

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 记忆传入参数

由于是延迟计算结果,所以要对参数进行记忆。
这里的实现方式是采用闭包。

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 总结

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

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

1.5 扩展

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

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.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.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三个参数不用每次都写。

后记

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant