JavaScript的闭包机制

一、闭包概述

1.1、闭包的概念

    闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

2.1、执行环境与作用域链

    当某个函数被调用时,会创建一个执行环境及相应的作用域链

    然后,使用arguments和其他命名参数的值来初始化函数的活动对象

    但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境

        function compare(value1, value2){
            if(value1 < value2){
                return -1;
            }else if(value1 > value2){
                return 1;
            }else{
                return 0;
            }
        }

    全局环境的变量对象(window)始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。

    在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。

    当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。

    此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。

    对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。

    一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况有所不同。

2.3、闭包示例

        function createComparisonFunction(propertyName){
            return function(object1, object2){              //闭包
                var value1 = object1[propertyName];
                var value2 = object2[propertyName];

                if(value1 < value2){
                    return -1;
                }else if(value1 > value2){
                    return 1;
                }else{
                    return 0;
                }
            }
        }

        var compare = createComparisonFunction("name");
        var result = compare({
            name: "Nicholas"
        }, {
            name: "Greg"
        });

 

    createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。

    换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中。

    直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:

        //解除对匿名函数的引用(以便释放内存)
        compare = null;

2.4、闭包的缺点

    由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多。

二、闭包的特性

2.1、闭包中的变量

    闭包的一个副作用是:闭包只能取得包含函数中任何变量的最后一个值

        function createFunctions(){
            var result = new Array();

            for(var i = 0; i < 10; i++){
                result[i] = function(){   //result数组保存闭包
                    return i;
                };
            }

            return result;
        }

        var result = createFunctions();
        for(var j = 0; j < 10; j++){
            console.log(result[j]());     //都是打印'10'
        }

    因为result数组中的每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i

    可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示:

        function createFunctions(){
            var result = new Array();

            for(var i = 0; i < 10; i++){
                result[i] = function(num){   
                    return function(){
                        return num;
                    };
                }(i);                           //函数对象后跟括号,函数会立即执行
            }

            return result;
        }

        var result = createFunctions();
        for(var j = 0; j < 10; j++){
            console.log(result[j]());     //0,1,2,3,4,5,6,7,8,9
        }

    在这个版本中,没有直接把闭包赋值给数组,而是定义了一个匿名函数,并立即执行该匿名函数。

    由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num

    而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。

2.2、闭包中的this对象

    闭包的执行环境具有全局性,因此其this对象通常指向window

        var name = "The Window";

        var object = {
            name: "My Object",

            getNameFunc: function(){
                return function(){
                    return this.name;
                };
            }
        };

        console.log(object.getNameFunc()());        //"The Window"

    每个函数在被调用时都会自动取得两个特殊变量:thisarguments

    内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量

    但是,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示:

        var name = "The Window";

        var object = {
            name: "My Object",

            getNameFunc: function(){
                var that = this;
                return function(){
                    return that.name;
                };
            }
        };

        console.log(object.getNameFunc()());        //"My Object"

    arguments也存在同样的问题。如果想访问外部作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。

2.3、闭包导致的内存泄露

    如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。

        function assignHandler(){
            var element = document.getElementById("someElement");
            element.onclick = function(){           //闭包
                console.log(element.id);            //循环引用了element对象
            }
        }

    由于闭包中保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。

    只要闭包存在,element的引用数至少也是1。

    不过,这个问题可以通过稍微改写一下代码来解决,如下所示:

        function assignHandler(){
            var element = document.getElementById("someElement");
            var id = element.id;

            element.onclick = function(){           //闭包
                console.log(id);            
            };

            element = null;             
        }

    把element变量设置为null,这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

三、闭包实现的功能

3.1、使用闭包模仿块级作用域

    JavaScript没有块级作用域的概念,但是可以使用闭包模仿块级作用域。

        (function(){
            //这里是块级作用域
        })();

    因为JavaScript将function关键字当作函数声明的开始,而函数声明后面不能跟圆括号。

    然而,函数表达式的后面可以跟圆括号,所以为函数加上圆括号,使其转换成函数表达式。

        (function(){
            var i = 0;
        })();

        console.log(i);         //报错

    由于变量i是在块级作用域里面声明的,所以在全局作用域中访问不到它。

3.2、私有变量

    JavaScript没有私有成员的概念,所有对象属性都是公有的。

    任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。

    私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

        function add(num1, num2){
            var sum = num1 + num2;
            return sum;
        }

    在上面的函数add中,有3个私有变量:num1num2sum

    如果在函数内部创建一个闭包,那么闭包通过自己的作用域链可以访问私有变量。

    把有权访问私有变量和私有函数的公有方法称为特权方法

        function MyObject(){

            //私有变量
            var privateVariable = 10;

            //私有函数
            function privateFunction(){
                return false;
            }

            //特权方法
            this.publicMethod = function(){
                privateVariable++;
                return privateFunction();
            };
        }

    这种方式构建的私有变量不能由所有实例共享,且每个实例都会创建一个特权方法,不能实现函数复用

3.3、静态私有变量

    在私有作用域中定义私有变量或函数,同样也可以创建特权方法。

        (function(){
            
            //私有变量
            var name = "";
            
            //构造函数
            Person = function(value){
                name = value;
            };

            //特权方法
            Person.prototype.getName = function(){
                return name;
            };
            
            //特权方法
            Person.prototype.setName = function(value){
                name = value;
            }
        })();

        var person1 = new Person("Nicholas");
        console.log(person1.getName());         //"Nicholas"
        person1.setName("Greg");    
        console.log(person1.getName);           //"Greg"

        var person2 = new Person("Michael");
        console.log(person1.getName());         //"Michael"
        console.log(person2.getName());         //"Michael"

    Person在块级作用域中被声明为全局变量,能够在块级作用域之外被访问到。

    特权方法作为闭包,总是保存着对包含作用域的引用。

    这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的,而且实现了特权方法的函数复用。

    以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量

3.4、模块模式

    前面的模式是用于为自定义类型创建私有变量和特权方法的,而模块模式则是为单例创建私有变量和特权方法。

    单例,指的就是只有一个实例的对象。通常,JavaScript是以对象字面量的方式来创建单例对象的:

        var singleTon = {
            name: value,
            method: function(){
                //这里是方法的代码
            }
        };

    模块模式通过为单例添加私有变量和特权方法能够使其得到增强

        var application = function(){
            
            //私有变量和函数
            var components = new Array();

            //初始化
            components.push(new BaseComponent());

            //公共
            return {
                getComponentCount: function(){
                    return components.length;
                },

                registerComponent: function(component){
                    if(typeof component == "object"){
                        components.push(component);
                    }
                }
            };
        }();        //立即调用执行!

    在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。

    上面这个简单的例子创建了一个用于管理组件的application对象。

    在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例(在这里不需要关心BaseComponent的代码,我们只是用它来展示初始化操作)。

    而返回对象的getComponentCount()registerComponent()方法,都是有权访问数组components的特权方法。前者返回已注册的组件数目,后者用于注册新组件。

    模块模式的适用条件:如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是Object的实例,因为最终要通过一个对象字面量来表示它。

3.5、增强的模块模式

    对模块模式进行改进,即在返回对象之前加入对其增强的代码。

    这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况

    如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码:

        var application = function(){

            //私有变量和函数
            var components = new Array();

            //初始化
            components.push(new BaseComponent());

            //创建application的一个局部副本
            var app = new BaseComponent();

            //公共接口
            app.getComponentCount = function(){
                return components.length;
            };

            app.registerComponent = function(component){
                if(typeof component == "object"){
                    components.push(component);
                }
            };

            //返回这个副本
            return app;
        }();

猜你喜欢

转载自blog.csdn.net/qq_35732147/article/details/82836931