zoukankan      html  css  js  c++  java
  • JavaScript的柯里化和反柯里化

    前言

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

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

    一、柯里化

    1.1 例子

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

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

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

    完整代码如下:

     1 function currying(fn){
     2     var allArgs = [];
     3 
     4     return function next(){
     5         var args = [].slice.call(arguments);
     6 
     7         if(args.length > 0){
     8             allArgs = allArgs.concat(args);
     9             return next;
    10         }else{
    11             return fn.apply(null, allArgs);
    12         }
    13     } 
    14 }
    15 var add = currying(function(){
    16     var sum = 0;
    17     for(var i = 0; i < arguments.length; i++){
    18         sum += arguments[i];
    19     }
    20     return sum;
    21 });

    1.2 记忆传入参数

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

     1 function currying(fn){
     2     var allArgs = [];
     3 
     4     return function next(){
     5         var args = [].slice.call(arguments);
     6 
     7         if(args.length > 0){
     8             allArgs = allArgs.concat(args);
     9             return next;
    10         }
    11     } 
    12 }

    当执行var add = currying(...)时,add变量已经指向了next方法。此时,allArgsnext方法内部有引用到,所以不能被GC回收。也就是说,allArgs在该赋值语句执行后,一直存在,形成了闭包。
    依靠这个特性,只要把接收的参数,不断放入allArgs变量进行存储即可。
    所以,当arguments.length > 0时,就可以将接收的新参数,放到allArgs中。
    最后返回next函数指针,形成链式调用。

    1.3 判断触发函数执行条件

    题意是,空参数时,输出结果。所以,只要判断arguments.length == 0即可执行。
    另外,由于计算结果的方法,是作为参数传入currying函数,所以要利用apply进行执行。
    综合上述思考,就可以得到以下完整的柯里化函数。

     1 function currying(fn){
     2     var allArgs = []; // 用来接收参数
     3 
     4     return function next(){
     5         var args = [].slice.call(arguments);
     6 
     7         // 判断是否执行计算
     8         if(args.length > 0){
     9             allArgs = allArgs.concat(args); // 收集传入的参数,进行缓存
    10             return next;
    11         }else{
    12             return fn.apply(null, allArgs); // 符合执行条件,执行计算
    13         }
    14     } 
    15 }

    1.4 总结

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

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

    1.5 扩展

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

     1 function currying(fn){
     2     var allArgs = [];
     3 
     4     function next(){
     5         var args = [].slice.call(arguments);
     6         allArgs = allArgs.concat(args);
     7         return next;
     8     }
     9     // 字符类型
    10     next.toString = function(){
    11         return fn.apply(null, allArgs);
    12     };
    13     // 数值类型
    14     next.valueOf = function(){
    15         return fn.apply(null, allArgs);
    16     }
    17 
    18     return next;
    19 }
    20 var add = currying(function(){
    21     var sum = 0;
    22     for(var i = 0; i < arguments.length; i++){
    23         sum += arguments[i];
    24     }
    25     return sum;
    26 });

    二、反柯里化

    2.1 例子

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

     1 // 轻提示
     2 function Toast(option){
     3   this.prompt = '';
     4 }
     5 Toast.prototype = {
     6   constructor: Toast,
     7   // 输出提示
     8   show: function(){
     9     console.log(this.prompt);
    10   }
    11 };
    12 
    13 // 新对象
    14 var obj = {
    15     prompt: '新对象'
    16 };

    用反柯里化的方式,可以这么做

     1 function unCurrying(fn){
     2     return function(){
     3         var args = [].slice.call(arguments);
     4         var that = args.shift();
     5         return fn.apply(that, args);
     6     }
     7 }
     8 
     9 var objShow = unCurrying(Toast.prototype.show);
    10 
    11 objShow(obj); // 输出"新对象"

    2.2 反柯里化的行为

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

    在上面的例子中,Toast.prototype.show方法,本来是Toast类的私有方法。跟新对象obj没有半毛钱关系。
    经过反柯里化后,却可以为obj对象所用。
    为什么能被obj所用,是因为内部将Toast.prototype.show的上下文重新定义为obj。也就是用apply改变了this指向。
    而实现这一步骤的过程,就需要增加反柯里化后的objShow方法参数。

    2.3 另一种反柯里化的实现

     1 Function.prototype.unCurrying = function(){
     2     var self = this;
     3     return function(){
     4         return Function.prototype.call.apply(self, arguments);
     5     }
     6 }
     7 
     8 // 使用
     9 var objShow = Toast.prototype.show.unCurrying();
    10 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 判断变量类型(反柯里化)

     1 var fn = function(){};
     2 var val = 1;
     3 
     4 if(Object.prototype.toString.call(fn) == '[object Function]'){
     5     console.log(`${fn} is function.`);
     6 }
     7 
     8 if(Object.prototype.toString.call(val) == '[object Number]'){
     9     console.log(`${val} is number.`);
    10 }

    上述代码,用反柯里化,可以这么写:

     1 var fn = function(){};
     2 var val = 1;
     3 var toString = Object.prototype.toString.unCurrying();
     4 
     5 if(toString(fn) == '[object Function]'){
     6     console.log(`${fn} is function.`);
     7 }
     8 
     9 if(toString(val) == '[object Number]'){
    10     console.log(`${val} is number.`);
    11 }

    3.2 监听事件(柯里化)

     1 function nodeListen(node, eventName){
     2     return function(fn){
     3         node.addEventListener(eventName, function(){
     4             fn.apply(this, Array.prototype.slice.call(arguments));
     5         }, false);
     6     }
     7 }
     8 
     9 var bodyClickListen = nodeListen(document.body, 'click');
    10 bodyClickListen(function(){
    11     console.log('first listen');
    12 });
    13 
    14 bodyClickListen(function(){
    15     console.log('second listen');
    16 });

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

    后记

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


    转载:https://www.imooc.com/article/46624

  • 相关阅读:
    微信小程序实战练习(仿五洲到家微信版)
    vue2.0项目 calendar.js(日历组件封装)
    基于thinkphp的后台管理系统模板快速搭建
    你不知道的javascript(上卷)读后感(二)
    你不知道的javascript(上卷)读后感(一)
    教你10分钟搭建酷炫的个人博客
    Webpack学习-工作原理(下)
    Webpack学习-工作原理(上)
    Css Secret 案例Demo全套
    基于excel导入数据到ms sql server
  • 原文地址:https://www.cnblogs.com/zhihaospace/p/12269087.html
Copyright © 2011-2022 走看看