JavaScript设计模式与开发实践 闭包和高级函数

1、闭包

1.1 变量的生命周期

  除了变量的作用域,另一个域闭包有关的概念是变量额生存周期。

  函数内部用var声明的局部变量,退出函数时,会随着函数调用的结束而被销毁.

var func = function() {
    var a = 1;
    alert(a);
}

func();
    var func = function(){
        var a = 1;
        return function(){
            a++;
            alert ( a );
        }
    };

    var f = func();

    f(); // 输出:2
    f(); // 输出:3
    f(); // 输出:4
    f(); // 输出:5

  这里当执行var f = func()后,f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里,既然局部变量所在的环境还能被外界访问,这个局部变量就有不被销毁的理由。

var nodes = document.getElemensByTagName('div');

for(var i = 0; i < node.length; i++) {
    (function(i){
        nodes[i].onclick = function() {
            alert(i)  
        }
   })(i)
}

  使用闭包把每次循环的i封闭,当事件函数顺着作用域链从内到外查找i,会先找到在被封闭在闭包环境的i。

1.2 闭包的更多作用

1.封装变量

  闭包可以把不需要再全局的变量封装成“私有变量”。

    //假设有一个计算乘积的简单函数:
    var mult = function(){
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i];
        }
        return a;
    };
    //我们可以加入缓存机制来提高这个函数的性能:
    //将输入的参数和输出的结果保存到cache对象
    var cache = {};
    var mult = function(){
        var args = Array.prototype.join.call( arguments, ',' );
        //每次执行先查询cache里有没执行过使用这个参数
        if ( cache[ args ] ){
            return cache[ args ];
        }

        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i];
        }
        return cache[ args ] = a;
    };

    alert ( mult( 1,2,3 ) ); // 输出:6
    alert ( mult( 1,2,3 ) ); // 输出:6
    //将cache放在mult内部,避免引发错误
    var mult = (function(){
        var cache = {};
        return function(){
            var args = Array.prototype.join.call( arguments, ',' );
            if ( args in cache ){
                return cache[ args ];
            }
            var a = 1;
            for ( var i = 0, l = arguments.length; i < l; i++ ){
                a = a * arguments[i];
            }
            return cache[ args ] = a;
        }
    })();
    //把能够独立出来的代码封装在独立地小函数
    var mult = (function(){
        var cache = {};
        var calculate = function(){ // 封闭calculate 函数
            var a = 1;
            for ( var i = 0, l = arguments.length; i < l; i++ ){
                a = a * arguments[i];
            }
            return a;
        };

        return function(){
            var args = Array.prototype.join.call( arguments, ',' );
            if ( args in cache ){
                return cache[ args ];
            }

            return cache[ args ] = calculate.apply( null, arguments );
        }

    })();

2、 延续局部变量寿命

    //img 对象经常用于进行数据上报
    var report = function( src ){
    var img = new Image();
        img.src = src;
    };
    report( 'http://xxx.com/getUserInfo' );

img是report的局部变量,report函数执行完,img局部变量会被销毁,可能没来得及发出HTTP请求。

现在我们把img 变量用闭包封闭起来,便能解决请求丢失的问题:

    
    var report = (function(){
        var imgs = [];
        return function( src ){
            var img = new Image();
            imgs.push( img );
            img.src = src;
        }
    })();

闭包和面向对象设计

    //下面来看看这段跟闭包相关的代码:
    var extent = function(){
        var value = 0;
        return {
            call: function(){
                value++;
                console.log( value );
            }
        }
    };

    var extent = extent();
    extent.call(); // 输出:1
    extent.call(); // 输出:2
    extent.call(); // 输出:3
    //如果换成面向对象的写法,就是:
    var extent = {
        value: 0,
        call: function(){
            this.value++;
            console.log( this.value );
        }
    };

    extent.call(); // 输出:1
    extent.call(); // 输出:2
    extent.call(); // 输出:3
    //或者:

    var Extent = function(){
        this.value = 0;
    };

    Extent.prototype.call = function(){
        this.value++;
        console.log( this.value );
    };

    var extent = new Extent();

    extent.call();
    extent.call();
    extent.call();

命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接收者(接收者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。

但在JavaScript,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更自然。如果需要往函数对象中预先植入 命令的接收者,可以使用闭包。

    var Tv = {
        open: function(){
            console.log( '打开电视机' );
        },

        close: function(){
            console.log( '关上电视机' );
        }
    };

    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 ) );

2 高阶函数

高阶函数是指至少满足下列条件之一的函数:

  1. 函数可以作为参数被传递
  2. 函数可以作为返回值输出

2.1 函数作为参数传递

1 回调函数

  AJAX异步请求中,回调函数应用非常频繁。我们想在AJAX请求返回都做一些事,但不知道请求返回的时间,最常用的方案是把callback函数当做参数传入AJAX请求的方法。

    var getUserInfo = function( userId, callback ){
        $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){
            if ( typeof callback === 'function' ){
                callback( data );
            }
        });
    }
    getUserInfo( 13157, function( data ){
        alert ( data.userName );
    });

  当一些函数不适合执行一些请求,可以把这些请求封装成一个函数,并把它作为参数传递给另一个函数,“委托”给另一个函数执行。

  例如,在页面创建100个节点并把它们设置为隐藏,但每次操作都要把它们隐藏,因此可以把隐藏操作的代码抽出。

    var appendDiv = function( callback ){
        for ( var i = 0; i < 100; i++ ){
            var div = document.createElement( 'div' );
            div.innerHTML = i;
            document.body.appendChild( div );
            if ( typeof callback === 'function' ){
                callback( div );
            }
        }
    };
    appendDiv(function( node ){
        node.style.display = 'none';
    });

2 Array.prototype.sort

Array.prototype.sort接受一个函数作为参数,这个函数封装了数组的排序规则。

    //从小到大
        [ 1, 4, 3 ].sort( function( a, b ){
        return a - b;
    });

        //从大到小
    [ 1, 4, 3 ].sort( function( a, b ){
        return b - a;
    });

2.2函数作为返回值输出

1.判断数据的类型

    var isString = function( obj ){
        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]';
    };

将相同的部分提取,只有Object.prototype.toString.call(obj)返回值不同。

var isType = function(type) {
    return function(obj) {
        return Object.prototype.toString.call( obj ) === '[object '+ type +']';
   }
}

可以用循环语句注册

    var Type = {};
    for ( var i = 0, type; type = [ 'String', 'Array', 'Number' ][ i++ ]; ){
        (function( type ){
            Type[ 'is' + type ] = function( obj ){
                return Object.prototype.toString.call( obj ) === '[object '+ type +']';
            }
        })( type )
    };

2.3高阶函数其他应用

1  函数节流(throttle)

 定义

  如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。

  也就是会说预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。

为什么需要函数节流

  在某些情况下,函数可能被频繁调用而造成大的性能问题。

  1. window.onresize事件。如果在window上绑定了resize事件,当拖动浏览器窗口来改变大小,这个事件触发的频率很高,如果resize事件函数里有DOM操作,可能造成浏览器卡顿。
  2. mousemove事件。如果给DOM节点绑定了拖曳事件(主要是mousemove),当div节点被拖动,也会频繁触发该事件。 
  3. 上传进度。 

实现

下面的throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行完成,则忽略接下来调用该函数的请求。throttle函数接受2个参数,第一个为需要被延迟执行的函数,第二个为延迟执行的事件。  

    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.log( 1 );
    }, 500 );

  

2 分时函数

  在短时间内忘页面大量添加DOM节点会让浏览器卡顿甚至假死。例如WebQQ的QQ好友列表,如果一个好友用一个节点表示,可能会创建成百上千的节点。

  使用timeChunk函数让创建节点分批进行,如一秒创建1000个节点改为每隔200毫秒创建8个节点。

    //第一个参数是创建节点所需数据,第二个参数封装了创建节点逻辑的函数,第三个参数表示每一批创建的节点数据
    var timeChunk = function( ary, fn, count ){
        var obj,
        t;
        var 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 = n;
        document.body.appendChild( div );
    }, 8 );
    renderFriendList();

3 惰性加载函数

一般我们定义浏览器通用的事件绑定函数addEvent如下。它的缺点是,每次被调用都会执行if分句。

    var addEvent = function( elem, type, handler ){
        if ( window.addEventListener ){
            return elem.addEventListener( type, handler, false );

        }
        if ( window.attachEvent ){
            return elem.attachEvent( 'on' + type, handler );
        }
    };

改进后,把嗅探浏览器操作提前到代码加载时,在加载时进行一次判断,让addEvent返回一个包裹了正确逻辑的函数。

但它仍然有缺点。如果外面没有调用过addEvent函数,它就进行了多余的操作。

    var addEvent = (function(){
        if ( window.addEventListener ){
            return function( elem, type, handler ){
                elem.addEventListener( type, handler, false );
            }
        }
        if ( window.attachEvent ){
            return function( elem, type, handler ){
                elem.attachEvent( 'on' + type, handler );
            }
        }
    })();

使用惰性加载,在第一次进入分支后,函数内部重写这个函数,下次进入addEvent函数就不存在条件分支语句。

            var addEvent = function( elem, type, handler ){
                if ( window.addEventListener ){
                    addEvent = function( elem, type, handler ){
                        elem.addEventListener( type, handler, false );
                    }
                }else if ( window.attachEvent ){
                    addEvent = function( elem, type, handler ){
                        elem.attachEvent( 'on' + type, handler );
                    }
                }
                addEvent( elem, type, handler );
            };
原文地址:https://www.cnblogs.com/surahe/p/6004463.html