美文网首页浓缩解读前端系列书籍
浓缩解读《JavaScript设计模式与开发实践》③

浓缩解读《JavaScript设计模式与开发实践》③

作者: 梁同学de自言自语 | 来源:发表于2017-01-12 00:39 被阅读465次

    三、闭包和高阶函数

    IMG_20170112_004128.jpg

    3.1 闭包

    3.1.1 变量的作用域

    • 所谓变量的作用域,就是变量的有效范围。通过作用域的划分,JavaScript变量分为全局变量和局部变量。
    • 声明在函数外的变量为全局变量;在函数内并且以var关键字声明的变量为局部变量
    • 我们都知道,全局变量能在任何作用域访问到,但这很容易造成命名冲突;而局部变量只有在函数里面能访问到,这是因为JavaScript的查找变量的规则是从内往外搜索的。

    3.1.2 变量的生命周期

    • 全局变量的生命周期是永久的(除非我们主动销毁这个全局变量),而局部变量则当函数执行完毕时被销毁。
    • 那JavaScript中是否存在,即便函数执行完毕,依然不会被销毁的局部变量?答案是肯定的。
    <script type="text/javascript">
        //现在有一个名为func的函数
        var func = function(){
            //①函数执行体中,将局部变量a赋值为1
            var a = 1;
            //②返回一个function执行环境
            return function(){
                //③执行环境中,将func.a局部变量加1,然后输出到控制台
                a++;
                console.info(a);
            }
        };
        
        //调用:将func函数执行后的返回,赋值给f
        var f = func();
        f();    //f()调用一次,输出2
        f();    //f()再调用一次,输出3
        f();    //f()接着调用,输出4
    </script>
    
    • 以上案例中的func.a局部变量,在func()函数执行过后并没有被销毁。每次执行f()时,仍能对它进行累加,就是佐证。这是因为func()返回了一个匿名函数的引用赋值给f,正是由于被外部变量引用了,所以不被销毁,此时这个匿名函数就称为闭包
    • 什么是闭包?

      在一个函数内定义另外一个函数(内部函数可以访问外部函数的变量),如果将这个内部函数提供给其他变量引用时,内部函数作用域以及依赖的外部作用域的执行环境就不会被销毁。此时这个内部函数就像一个可以访问封闭数据包的执行环境,也就是闭包。

    3.1.3 闭包的用途

    • 我们不但要学习什么是JavaScript闭包,更要了解如何利用闭包特性来写代码。由于篇幅有限,书中只罗列了几个使用闭包的例子,但要知道实际开发中运用闭包非常广泛,远不止于此。
    1. 封装变量:通过闭包将不需要暴露的变量封装成“私有变量”
    var person = (function(){
        var name = "William";
        return function(){
            console.info(name);          
        };
    })();
    person();   // 输出成功
    console.info(person.name);  //// 输出失败
    
    1. 延续变量的生命周期:我们经常用<img>标签进行数据上报,创建一个临时的img标签,将需要上报的数据附加在img的url后缀,从而上送到服务器。如例子所示:
    var report = function(dataSrc){
        var img = new Image();  //创建image对象
        img.src = dataSrc;  //将要上送的数据url赋值给img的url
    };
    report('http://xxx.com/uploadUserData?name=william');
    
    • 可经过排查发现,使用report()函数存在30%丢失数据的情况。这是因为,imgreport()函数中的局部变量,函数执行完毕后就被销毁了,而这个时候往往HTTP请求还没建立成功。而通过闭包来保存img变量可以解决请求丢失的问题:
    //注意:我们将普通函数改成了自执行函数
    var report = (function(){
        var imgs = [];
        return function(dataSrc){
            var img = new Image();
            images.push(img);
            img.src = dataSrc;
        }
    })();
    
    1. 用闭包实现面向对象:我们经常使用过程数据来描述面向对象编程当中的对象。对象的方法包含了过程,而闭包则是在过程中以执行环境的方式包含了数据。
    • 既然闭包可以封装私有变量,自然也能完成面向对象的设计。实际上,用面向对象思想能实现的功能,用闭包也能实现,反之亦然,这就是JavaScript的灵活之处。
    • 有这样一段面向对象的JS代码:
    //Person构造器,里面有一个name属性
    var Person = function(){
      this.name = "William";
    };
    //给Person的原型添加一个sayName()方法
    Person.prototype.sayName = function(){
        console.info("hello,my name is " + this.name);
    };
    //实例化Person
    var person1 = new Person();
    person1.sayName();
    
    • 用闭包可以实现同样的效果:因为在JavaScript用new执行构造函数,本质也是返回一个对象
    //person()函数返回一个有sayName()方法的对象
    var person = function(){
        var name = "William";
        return {
            sayName : function(){
                console.info("hello,my name is " + name);
            }
        }
    };
    //执行person()函数,将返回的对象赋值给person1
    var person1 = person();
    //调用person1.sayName()方法
    person1.sayName();
    //控制台输出 "hello,my name is William"
    
    1. 用闭包实现命令模式
    • 命令模式是将请求封装成对象,从而可以把不同的请求对象进行参数化、对请求对象排队或者记录日志以及执行可撤销的操作。
    • 命令模式的能够分离请求发起者和执行者之间的耦合关系。往往在命令被执行之前,就预先往命令对象中植入命令的执行者。
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title></title>
        </head>
        <body>
            <button id="execute">开启</button>
            <button id="undo">关闭</button>
            <script type="text/javascript">
                var Tv = {
                    open : function(){
                        console.info("打开电视机");
                    },
                    close : function(){
                        console.info("关闭电视机");
                    }
                };
                var OpenTvCommand = function(receiver){
                    this.receiver = receiver;
                };
                OpenTvCommand.prototype.execute = function(){
                    this.receiver.open();
                };
                OpenTvCommand.prototype.undo = function(){
                    this.receiver.close();
                };
                var setCommand = function(command){
                    document.getElementById("execute").onclick = function(){
                        command.execute();
                    }
                    document.getElementById("undo").onclick = function(){
                        command.undo();
                    }
                };
                //调用
                setCommand(new OpenTvCommand(Tv));
            </script>
        </body>
    </html>
    
    • 用闭包实现命令模式:
    <script type="text/javascript">
        var Tv = {
            open : function(){
                console.info("打开电视机");
            },
            close : function(){
                console.info("关闭电视机");
            }
        };
        var createCommand = function(receiver){
            var execute = function(){
                return receiver.open();
            }
            var undo = function(){
                return receiver.close();
            }
            return {
                execute : execute,
                undo : undo
            }
        };
        var setCommand = function(command){
            document.getElementById("execute").onclick = function(){
                command.execute();
            }
            document.getElementById("undo").onclick = function(){
                command.undo();
            }
        };
        //调用
        setCommand(createCommand(Tv));
    </script>
    
    3.1.4 闭包与内存管理
    • 一直流传着一种耸人听闻的说法,声称闭包会造成内存泄漏,所以应当尽量避免使用闭包。
    • 局部变量本来应该在函数退出的时候被释放,但在闭包形成的环境中,局部变量不被释放。从这个意义上看,确实会造成一些数据无法被及时销毁。但我们使用闭包,是我们主动选择延长局部变量的生命周期,不能说成是内存泄漏。当使用完毕后,大可手动将这些变量设为null
    • 而只有闭包形成循环引用的情况下,才会导致内存泄漏。但这也不是闭包或者JavaScript的问题,我们可以避免循环引用的情况,而不是因噎废食,彻底摒弃闭包。

    3.2 高阶函数

    • 高阶函数是指满足以下两个条件之一的函数:
    1. 函数可以作为参数被传递;
    2. 函数可以作为返回值输出;
    • 显然,JavaScript语言中的函数两个条件都满足,下面将讲解JavaScript高阶函数特性的应用示例。。
    3.2.1 函数作为参数传入
    • 把函数当做参数传递,使得我们可以抽离出一部分容易变化的业务逻辑。
    • 这样的例子在JavaScript代码中比比皆是,比如jQuery中事件的绑定,或者jQuery中的ajax请求:
    //按钮监听事件
    $("btn").click(function(){
      console.info("btn clicked");
    });
    //可以发现,其本质就是执行了click()方法,然后传入一个函数作为参数。
    //注意到:在按钮点击后的处理是变化的,通过回调函数来封装变化。
    
    • 另外还有Array.sort()。这是用来排序数组的一个方法,传入一个自定义的函数来指定是递增还是递减排序。
    var arr = [1,7,9,2];
    //从小到大排序
    arr.sort(function(){
      return a - b;
    });
    console.info(arr); //输出 "[1, 2, 7, 9]"
    //从大道小排序
    arr.sort(function(){
      return b - a;
    });
    console.info(arr); //输出 "[9, 7, 2, 1]"
    
    3.2.2 函数作为返回值输出
    • 让函数返回一个可执行的函数,在之前的代码我们已经接触过了,这使得整个运算过程是可延续。
    • 我们通过优化一段类型判断的JavaScript代码来感受函数作为返回值输出的灵活:
    //判断是否为String
    var isString = function(obj){
        //通过传入的obj对象执行toString()方法,将结果值和预期字符串比较
        return Object.prototype.toString.call(obj) === '[object String]';
    }
    //判断是否为数组
    var isArray = function(obj){
        return Object.prototype.toString.call(obj) === '[object Array]';
    }
    //判断是否为数字
    var isNumber = function(obj){
        return Object.prototype.toString.call(obj) === '[object Number]';
    }
    
    • 可以发现上面的代码toString部分都是相同的,我们通过将函数作为返回值的方式优化代码。
    //抽象出一个类型判断的通用函数
    var isType = funcion(type){
        //该函数返回一个可执行的函数,用来执行toString方法和预期字符串做比较
        return function(obj){
            return Object.prototype.toString.call(obj) === '[Object '+type+']';
        }
    }
    //预先注册具体的类型判断方法
    var isString = isType("String");
    var isArray = isType("Array");
    var isNumber = isType("Number");
    //调用
    console.info(isArray([1,3,2]));  //输出: true
    
    • 另外一个例子,是利用JavaScript函数作为返回值这个特性实现单例模式。
    var getSingle = function(fn){
        var ret;  //临时变量
        return function(){
            //如果ret已经存在的话则返回;否则新创建对象
            return ret || (ret = fn.apply(this,arguments));
        }
    }
    var getScript = getSingle(function(){
        return document.createElement('script');
    });
    var script1 = getScript();
    var script2 = getScript();
    console.info(script1 === script2);//输出: true
    
    3.2.3 高阶函数实现AOP
    • AOP(面向切面编程)是指将日志统计、安全控制、异常处理等与业务逻辑无关的模块代码独立出来,通过“动态植入”的方式参入到业务逻辑模块当中。这样可以保持业务逻辑模块的纯净和高内聚性。
    • Java语言可以通过反射和动态代理机制来实现AOP技术,而JavaScript函数作为返回值的特性就可以简单的实现,这是JavaScript与生俱来的能力。
    Function.prototype.invokeBefore = function(beforFn){
        var _self = this;   //原函数的引用
        return function(){
            //先执行传入的before函数
            beforFn.apply(this,arguments);
            //然后再执行自身
            return _self.apply(this,arguments);
        }
    }
    Function.prototype.invokeAfter = function(afterFn){
        var _self = this;   //
        return function(){
            //先执行函数,并保存执行结果
            var ret = _self.apply(this,arguments);
            //然后再执行after函数
            afterFn.apply(this,arguments);
            //最后返回结果
            return ret;
        }
    }
    //定义一个方法,控制台输出2
    var func = function(){
        console.info(2);
    };
    //指定func()函数执行前和执行后要做的事情
    func = func.invokeBefore(function(){
        console.info(1);
    }).invokeAfter(function(){
        console.info(3);
    });
    //调用func()函数,控制台输出 1 2 3
    func();
    
    3.2.4 高阶函数实现柯里化
    • 函数柯里化(function currying)的概念是由注明数理逻辑学家Haskell Curry丰富和发展起来的,所以因此得名。
    • currying又称为部分求值。currying函数首先接受一些参数,接受这些参数之后并不立即求值,而是返回另外一个函数,并将传入的参数函数保存起来.等真正需要求值的时候,将之前传入的所有参数一次性的求值.
    • 我们通过JavaScript,通过一个记账的代码来模拟currying函数
    var currying = function(fn){
        var args = [];  //缓存对象
        return function(){
            if(arguments.length == 0){
                //如果传入的参数为空,则直接返回结果
                return fn.apply(this,args);
            }else{
                //如果参数不为空,则将传入参数push到args数组中缓存起来
                [].push.apply(args,arguments);
                //并返回函数本身
                return arguments.callee;
            }
        }
    }
    var cost = (function(){
        var money = 0;
        return function(){
            for(var i=0;l = arguments.length;i<l;i++){
                money += arguments[i];
            }
            return money;
        }
    });
    //转换成currying函数
    var cost = currying(cost);
    cost(100);  //记账100,未真正求值
    cost(100);  //记账100,未真正求值
    cost(400);  //记账400,未真正求值
    console.info(cost());   //求值,并输出:600
    
    3.2.5 高阶函数实现反柯里化
    • 通过call()apply()方法可以借用别的对象的方法,比如借用Array.prototype.push()方法.那么有没有办法将借用的方法提取出来呢?uncurrying就是用来解决这个问题的.
    //为Function对象的原型添加uncurrying方法
    Function.prototype.uncurrying = function(){
        var self = this;
        return function(){
            var obj = Array.prototype.call(arguments);
            return self.apply(obj,arguments);
        }
    }
    //提取push方法并使用
    var push = Array.prototype.uncurrying();
    (function(){
        push(arguments,4);
        console.info(arguments);//输出 [1,2,3,4]
    })(1,2,3);
    
    3.2.6 高阶函数实现函数节流
    • JavaScript中的函数大多数都是由用户主动触发的,尤其在浏览器端的某些情况下函数被非常频繁的调用,从而导致性能问题。
    • 比如用来监听浏览器窗口大小的window.onresize事件,当浏览器窗口被不断拉伸时,这个事件触发的频率会非常高;又比如元素的拖拽监听事件onmousemove,如果元素被不停的拖拽,也会频繁的触发;还有最典型的监听文件上传进度的事件,由于需要不断扫描文件用以在页面中显示扫描进度。导致通知的频率非常之高,大约一秒钟10次,远超过人眼所能觉察的极限。
    • throttle函数就是解决此类问题的方案。throttle顾名思义节流器,借鉴的是工程学里的思想,比如用节流器来稳定短距离的管道的水压或者气压,而在JavaScript中则是通过忽略短时间内函数的密集执行,达到稳定性能的作用。
    var throttle = function(fn,interval){
        var _self = fn,
                timer,
                firstTime = true;
        return function(){
            var args = arguments,
                    _me = this;
            if(firstTime){
                _self.apply(_me,args);
                return firstTime = false;
            }
            if(timer){
                return false;
            }
            timer = setTimeout(function(){
                clearTimeout(timer);
                timer = null;
                _self.apply(_me,args);
            },interval || 500);
        };
    };
    window.onresize = throttle(function(){
        console.info("resize come in");
    },500);
    
    3.2.7 高阶函数实现分时函数
    • 函数节流是限制函数被频繁调用的解决方案,但还有另外一种情况,某些不能忽略的频繁操作,同时也影响着页面的性能。比如WebQQ加载好友列表,往往需要短时间内一次性创建成百上千个节点,严重影响页面性能。
    //模拟添加1000个数据
    var ary = [];
    for (var i=1;i<=1000;i++) {
        ary.push(i);
    };
    var renderFriendList = function(data){
        for (var i=0;l=data.length;i<l;i++) {
            var div = document.createElement('div');
            div.innerHTML = i;
            document.body.appendChild(div);
        }
    };
    renderFriendList(ary);
    
    • 通过分时函数让创建节点的工作分批进行。
    //创建timeChunk函数
    var timeChunk = function(ary,fn,count){
        var obj,t,len = ary.length;
        var start = function(){
            for (var i=0;i<Math.min(count || 1,ary.length);i++) {
                var obj = ary.shift();
                fn(obj);
            }
        }
        return function(){
            t = setInterval(function(){
                if(ary.length === 0){
                    return clearInterval(t);
                }
                start();
            },200);
        }
    };
    //测试
    var ary = [];
    for (var i=1;i<=1000;i++) {
        ary.push(i);
    };
    var renderFriendList = timeChunk(ary,function(n){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    },8);
    renderFriendList(ary);
    
    • 除此之外,书中还有通过高阶函数的特性实现惰性加载函数的案例,考虑到文章篇幅的关系,这里就不赘述了。

    3.1 闭包

    3.1.1 变量的作用域

    • 所谓变量的作用域,就是变量的有效范围。通过作用域的划分,JavaScript变量分为全局变量和局部变量。
    • 声明在函数外的变量为全局变量;在函数内并且以var关键字声明的变量为局部变量
    • 我们都知道,全局变量能在任何作用域访问到,但这很容易造成命名冲突;而局部变量只有在函数里面能访问到,这是因为JavaScript的查找变量的规则是从内往外搜索的。

    3.1.2 变量的生命周期

    • 全局变量的生命周期是永久的(除非我们主动销毁这个全局变量),而局部变量则当函数执行完毕时被销毁。
    • 那JavaScript中是否存在,即便函数执行完毕,依然不会被销毁的局部变量?答案是肯定的。
    <script type="text/javascript">
        //现在有一个名为func的函数
        var func = function(){
            //①函数执行体中,将局部变量a赋值为1
            var a = 1;
            //②返回一个function执行环境
            return function(){
                //③执行环境中,将func.a局部变量加1,然后输出到控制台
                a++;
                console.info(a);
            }
        };
        
        //调用:将func函数执行后的返回,赋值给f
        var f = func();
        f();    //f()调用一次,输出2
        f();    //f()再调用一次,输出3
        f();    //f()接着调用,输出4
    </script>
    
    • 以上案例中的func.a局部变量,在func()函数执行过后并没有被销毁。每次执行f()时,仍能对它进行累加,就是佐证。这是因为func()返回了一个匿名函数的引用赋值给f,正是由于被外部变量引用了,所以不被销毁,此时这个匿名函数就称为闭包
    • 什么是闭包?

      在一个函数内定义另外一个函数(内部函数可以访问外部函数的变量),如果将这个内部函数提供给其他变量引用时,内部函数作用域以及依赖的外部作用域的执行环境就不会被销毁。此时这个内部函数就像一个可以访问封闭数据包的执行环境,也就是闭包。

    3.1.3 闭包的用途

    • 我们不但要学习什么是JavaScript闭包,更要了解如何利用闭包特性来写代码。由于篇幅有限,书中只罗列了几个使用闭包的例子,但要知道实际开发中运用闭包非常广泛,远不止于此。
    1. 封装变量:通过闭包将不需要暴露的变量封装成“私有变量”
    var person = (function(){
        var name = "William";
        return function(){
            console.info(name);          
        };
    })();
    person();   // 输出成功
    console.info(person.name);  //// 输出失败
    
    1. 延续变量的生命周期:我们经常用<img>标签进行数据上报,创建一个临时的img标签,将需要上报的数据附加在img的url后缀,从而上送到服务器。如例子所示:
    var report = function(dataSrc){
        var img = new Image();  //创建image对象
        img.src = dataSrc;  //将要上送的数据url赋值给img的url
    };
    report('http://xxx.com/uploadUserData?name=william');
    
    • 可经过排查发现,使用report()函数存在30%丢失数据的情况。这是因为,imgreport()函数中的局部变量,函数执行完毕后就被销毁了,而这个时候往往HTTP请求还没建立成功。而通过闭包来保存img变量可以解决请求丢失的问题:
    //注意:我们将普通函数改成了自执行函数
    var report = (function(){
        var imgs = [];
        return function(dataSrc){
            var img = new Image();
            images.push(img);
            img.src = dataSrc;
        }
    })();
    
    1. 用闭包实现面向对象:我们经常使用过程数据来描述面向对象编程当中的对象。对象的方法包含了过程,而闭包则是在过程中以执行环境的方式包含了数据。
    • 既然闭包可以封装私有变量,自然也能完成面向对象的设计。实际上,用面向对象思想能实现的功能,用闭包也能实现,反之亦然,这就是JavaScript的灵活之处。
    • 有这样一段面向对象的JS代码:
    //Person构造器,里面有一个name属性
    var Person = function(){
      this.name = "William";
    };
    //给Person的原型添加一个sayName()方法
    Person.prototype.sayName = function(){
        console.info("hello,my name is " + this.name);
    };
    //实例化Person
    var person1 = new Person();
    person1.sayName();
    
    • 用闭包可以实现同样的效果:因为在JavaScript用new执行构造函数,本质也是返回一个对象
    //person()函数返回一个有sayName()方法的对象
    var person = function(){
        var name = "William";
        return {
            sayName : function(){
                console.info("hello,my name is " + name);
            }
        }
    };
    //执行person()函数,将返回的对象赋值给person1
    var person1 = person();
    //调用person1.sayName()方法
    person1.sayName();
    //控制台输出 "hello,my name is William"
    
    1. 用闭包实现命令模式
    • 命令模式是将请求封装成对象,从而可以把不同的请求对象进行参数化、对请求对象排队或者记录日志以及执行可撤销的操作。
    • 命令模式的能够分离请求发起者和执行者之间的耦合关系。往往在命令被执行之前,就预先往命令对象中植入命令的执行者。
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <title></title>
        </head>
        <body>
            <button id="execute">开启</button>
            <button id="undo">关闭</button>
            <script type="text/javascript">
                var Tv = {
                    open : function(){
                        console.info("打开电视机");
                    },
                    close : function(){
                        console.info("关闭电视机");
                    }
                };
                var OpenTvCommand = function(receiver){
                    this.receiver = receiver;
                };
                OpenTvCommand.prototype.execute = function(){
                    this.receiver.open();
                };
                OpenTvCommand.prototype.undo = function(){
                    this.receiver.close();
                };
                var setCommand = function(command){
                    document.getElementById("execute").onclick = function(){
                        command.execute();
                    }
                    document.getElementById("undo").onclick = function(){
                        command.undo();
                    }
                };
                //调用
                setCommand(new OpenTvCommand(Tv));
            </script>
        </body>
    </html>
    
    • 用闭包实现命令模式:
    <script type="text/javascript">
        var Tv = {
            open : function(){
                console.info("打开电视机");
            },
            close : function(){
                console.info("关闭电视机");
            }
        };
        var createCommand = function(receiver){
            var execute = function(){
                return receiver.open();
            }
            var undo = function(){
                return receiver.close();
            }
            return {
                execute : execute,
                undo : undo
            }
        };
        var setCommand = function(command){
            document.getElementById("execute").onclick = function(){
                command.execute();
            }
            document.getElementById("undo").onclick = function(){
                command.undo();
            }
        };
        //调用
        setCommand(createCommand(Tv));
    </script>
    
    3.1.4 闭包与内存管理
    • 一直流传着一种耸人听闻的说法,声称闭包会造成内存泄漏,所以应当尽量避免使用闭包。
    • 局部变量本来应该在函数退出的时候被释放,但在闭包形成的环境中,局部变量不被释放。从这个意义上看,确实会造成一些数据无法被及时销毁。但我们使用闭包,是我们主动选择延长局部变量的生命周期,不能说成是内存泄漏。当使用完毕后,大可手动将这些变量设为null
    • 而只有闭包形成循环引用的情况下,才会导致内存泄漏。但这也不是闭包或者JavaScript的问题,我们可以避免循环引用的情况,而不是因噎废食,彻底摒弃闭包。

    3.2 高阶函数

    • 高阶函数是指满足以下两个条件之一的函数:
    1. 函数可以作为参数被传递;
    2. 函数可以作为返回值输出;
    • 显然,JavaScript语言中的函数两个条件都满足,下面将讲解JavaScript高阶函数特性的应用示例。。
    3.2.1 函数作为参数传入
    • 把函数当做参数传递,使得我们可以抽离出一部分容易变化的业务逻辑。
    • 这样的例子在JavaScript代码中比比皆是,比如jQuery中事件的绑定,或者jQuery中的ajax请求:
    //按钮监听事件
    $("btn").click(function(){
      console.info("btn clicked");
    });
    //可以发现,其本质就是执行了click()方法,然后传入一个函数作为参数。
    //注意到:在按钮点击后的处理是变化的,通过回调函数来封装变化。
    
    • 另外还有Array.sort()。这是用来排序数组的一个方法,传入一个自定义的函数来指定是递增还是递减排序。
    var arr = [1,7,9,2];
    //从小到大排序
    arr.sort(function(){
      return a - b;
    });
    console.info(arr); //输出 "[1, 2, 7, 9]"
    //从大道小排序
    arr.sort(function(){
      return b - a;
    });
    console.info(arr); //输出 "[9, 7, 2, 1]"
    
    3.2.2 函数作为返回值输出
    • 让函数返回一个可执行的函数,在之前的代码我们已经接触过了,这使得整个运算过程是可延续。
    • 我们通过优化一段类型判断的JavaScript代码来感受函数作为返回值输出的灵活:
    //判断是否为String
    var isString = function(obj){
        //通过传入的obj对象执行toString()方法,将结果值和预期字符串比较
        return Object.prototype.toString.call(obj) === '[object String]';
    }
    //判断是否为数组
    var isArray = function(obj){
        return Object.prototype.toString.call(obj) === '[object Array]';
    }
    //判断是否为数字
    var isNumber = function(obj){
        return Object.prototype.toString.call(obj) === '[object Number]';
    }
    
    • 可以发现上面的代码toString部分都是相同的,我们通过将函数作为返回值的方式优化代码。
    //抽象出一个类型判断的通用函数
    var isType = funcion(type){
        //该函数返回一个可执行的函数,用来执行toString方法和预期字符串做比较
        return function(obj){
            return Object.prototype.toString.call(obj) === '[Object '+type+']';
        }
    }
    //预先注册具体的类型判断方法
    var isString = isType("String");
    var isArray = isType("Array");
    var isNumber = isType("Number");
    //调用
    console.info(isArray([1,3,2]));  //输出: true
    
    • 另外一个例子,是利用JavaScript函数作为返回值这个特性实现单例模式。
    var getSingle = function(fn){
        var ret;  //临时变量
        return function(){
            //如果ret已经存在的话则返回;否则新创建对象
            return ret || (ret = fn.apply(this,arguments));
        }
    }
    var getScript = getSingle(function(){
        return document.createElement('script');
    });
    var script1 = getScript();
    var script2 = getScript();
    console.info(script1 === script2);//输出: true
    
    3.2.3 高阶函数实现AOP
    • AOP(面向切面编程)是指将日志统计、安全控制、异常处理等与业务逻辑无关的模块代码独立出来,通过“动态植入”的方式参入到业务逻辑模块当中。这样可以保持业务逻辑模块的纯净和高内聚性。
    • Java语言可以通过反射和动态代理机制来实现AOP技术,而JavaScript函数作为返回值的特性就可以简单的实现,这是JavaScript与生俱来的能力。
    Function.prototype.invokeBefore = function(beforFn){
        var _self = this;   //原函数的引用
        return function(){
            //先执行传入的before函数
            beforFn.apply(this,arguments);
            //然后再执行自身
            return _self.apply(this,arguments);
        }
    }
    Function.prototype.invokeAfter = function(afterFn){
        var _self = this;   //
        return function(){
            //先执行函数,并保存执行结果
            var ret = _self.apply(this,arguments);
            //然后再执行after函数
            afterFn.apply(this,arguments);
            //最后返回结果
            return ret;
        }
    }
    //定义一个方法,控制台输出2
    var func = function(){
        console.info(2);
    };
    //指定func()函数执行前和执行后要做的事情
    func = func.invokeBefore(function(){
        console.info(1);
    }).invokeAfter(function(){
        console.info(3);
    });
    //调用func()函数,控制台输出 1 2 3
    func();
    
    3.2.4 高阶函数实现柯里化
    • 函数柯里化(function currying)的概念是由注明数理逻辑学家Haskell Curry丰富和发展起来的,所以因此得名。
    • currying又称为部分求值。currying函数首先接受一些参数,接受这些参数之后并不立即求值,而是返回另外一个函数,并将传入的参数函数保存起来.等真正需要求值的时候,将之前传入的所有参数一次性的求值.
    • 我们通过JavaScript,通过一个记账的代码来模拟currying函数
    var currying = function(fn){
        var args = [];  //缓存对象
        return function(){
            if(arguments.length == 0){
                //如果传入的参数为空,则直接返回结果
                return fn.apply(this,args);
            }else{
                //如果参数不为空,则将传入参数push到args数组中缓存起来
                [].push.apply(args,arguments);
                //并返回函数本身
                return arguments.callee;
            }
        }
    }
    var cost = (function(){
        var money = 0;
        return function(){
            for(var i=0;l = arguments.length;i<l;i++){
                money += arguments[i];
            }
            return money;
        }
    });
    //转换成currying函数
    var cost = currying(cost);
    cost(100);  //记账100,未真正求值
    cost(100);  //记账100,未真正求值
    cost(400);  //记账400,未真正求值
    console.info(cost());   //求值,并输出:600
    
    3.2.5 高阶函数实现反柯里化
    • 通过call()apply()方法可以借用别的对象的方法,比如借用Array.prototype.push()方法.那么有没有办法将借用的方法提取出来呢?uncurrying就是用来解决这个问题的.
    //为Function对象的原型添加uncurrying方法
    Function.prototype.uncurrying = function(){
        var self = this;
        return function(){
            var obj = Array.prototype.call(arguments);
            return self.apply(obj,arguments);
        }
    }
    //提取push方法并使用
    var push = Array.prototype.uncurrying();
    (function(){
        push(arguments,4);
        console.info(arguments);//输出 [1,2,3,4]
    })(1,2,3);
    
    3.2.6 高阶函数实现函数节流
    • JavaScript中的函数大多数都是由用户主动触发的,尤其在浏览器端的某些情况下函数被非常频繁的调用,从而导致性能问题。
    • 比如用来监听浏览器窗口大小的window.onresize事件,当浏览器窗口被不断拉伸时,这个事件触发的频率会非常高;又比如元素的拖拽监听事件onmousemove,如果元素被不停的拖拽,也会频繁的触发;还有最典型的监听文件上传进度的事件,由于需要不断扫描文件用以在页面中显示扫描进度。导致通知的频率非常之高,大约一秒钟10次,远超过人眼所能觉察的极限。
    • throttle函数就是解决此类问题的方案。throttle顾名思义节流器,借鉴的是工程学里的思想,比如用节流器来稳定短距离的管道的水压或者气压,而在JavaScript中则是通过忽略短时间内函数的密集执行,达到稳定性能的作用。
    var throttle = function(fn,interval){
        var _self = fn,
                timer,
                firstTime = true;
        return function(){
            var args = arguments,
                    _me = this;
            if(firstTime){
                _self.apply(_me,args);
                return firstTime = false;
            }
            if(timer){
                return false;
            }
            timer = setTimeout(function(){
                clearTimeout(timer);
                timer = null;
                _self.apply(_me,args);
            },interval || 500);
        };
    };
    window.onresize = throttle(function(){
        console.info("resize come in");
    },500);
    
    3.2.7 高阶函数实现分时函数
    • 函数节流是限制函数被频繁调用的解决方案,但还有另外一种情况,某些不能忽略的频繁操作,同时也影响着页面的性能。比如WebQQ加载好友列表,往往需要短时间内一次性创建成百上千个节点,严重影响页面性能。
    //模拟添加1000个数据
    var ary = [];
    for (var i=1;i<=1000;i++) {
        ary.push(i);
    };
    var renderFriendList = function(data){
        for (var i=0;l=data.length;i<l;i++) {
            var div = document.createElement('div');
            div.innerHTML = i;
            document.body.appendChild(div);
        }
    };
    renderFriendList(ary);
    
    • 通过分时函数让创建节点的工作分批进行。
    //创建timeChunk函数
    var timeChunk = function(ary,fn,count){
        var obj,t,len = ary.length;
        var start = function(){
            for (var i=0;i<Math.min(count || 1,ary.length);i++) {
                var obj = ary.shift();
                fn(obj);
            }
        }
        return function(){
            t = setInterval(function(){
                if(ary.length === 0){
                    return clearInterval(t);
                }
                start();
            },200);
        }
    };
    //测试
    var ary = [];
    for (var i=1;i<=1000;i++) {
        ary.push(i);
    };
    var renderFriendList = timeChunk(ary,function(n){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    },8);
    renderFriendList(ary);
    
    • 除此之外,书中还有通过高阶函数的特性实现惰性加载函数的案例,考虑到文章篇幅的关系,这里就不赘述了。

    相关文章

      网友评论

        本文标题:浓缩解读《JavaScript设计模式与开发实践》③

        本文链接:https://www.haomeiwen.com/subject/kncdbttx.html