JavaScript闭包什么鬼

知道91 | 前端 | 2015-10-08 | 阅读:3136

官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

javascript的闭包是非常有用的特性,利用它可以实现很多功能,在学习闭包的过程中也想分享自己的一些理解和总结,于是写了此文。

闭包是什么? ( 相关概念:匿名函数,执行环境,作用域链,变量对象,活动对象,this对象,内存泄露)。

  闭包(closure):有权访问另一个函数作用域中的变量的函数;

  匿名函数(anonymous function):创建一个函数并将它赋值给变量functionName;

  执行环境(execution context):定义了变量和函数访问其他数据和决定它们各自行为的权限;

  变量对象(variable object):执行环境中定义的所有变量和函数都保存在这个对象中,编写代码时无法访问,但是解析器中处理数据时会在后台使用到它;

  作用域链(scope chain):当代码中一个环境中执行时,会创建变量对象的一个作用域链,保证对执行环境有权访问的变量和函数进行有序访问;本质上是一个指向变量对象的指 针列表,它只引用但不实际包含变量对象;

  活动对象(activation object):当前执行环境的变量对象,如果环境是函数,则活动对象为函数的aguments对象;

  This对象:在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。

为什么要使用闭包?

  为了防止命名冲突和恶意篡改,通常不定义全局变量,用闭包来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作;避免全局变量灾难,实现数据安全。

闭包有什么优缺点?

如下所示匿名函数为一个闭包:

function createFunction () {
    var result = new Array();
    for (var i=0; i<10; i++) {
        result[i] = function() {  //闭包
            return i;
        };
    }
}

但是这样的闭包并没有突出它的优势,如果改成下面这样:

function createFunction () {
    var result = new Array();
    for (var i=0; i<10; i++) {
        result[i] = function() {
            return i;
        };
    }
}
var fun = createFunction();
fun();

这样,可以在外部通过父函数的返回值来获取内部函数的引用,不再局限于父函数。还有其他方法,各位可以自行研究。

另一个好处:

  内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

  原因:当某个函数被调用时,会创建一个执行环境及相应的作用域链,然后使用arguments和其他命名参数的值来初始化函数的活动对象。在作用域链中,外部函数的活动对象处于第二位,外部函数的外部函数的活动对象处于第三位……一直到作用域链终点——全局执行环境。至此还需要了解此过程的作用域链是怎么样的。

function compareFunction (propertyName) {
    return function compare (value1, value2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    }
}

在函数执行中,就会在作用域链中查找变量,以写入和读取变量的值;无论什么时候在函数中访问一个变量,都会在作用域链中搜索,一般当函数执行完毕后,内部活动对象就会被销毁,内存中仅仅保存全局作用域,但是闭包的情况却有不同。在一个函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中,内部函数被外部函数返回后,内部匿名函数的作用域链被初始化成包含外部函数的活动对象和全局变量对象,这样内部匿名函数就可以访问外部函数的所有变量。不同的是,当外部函数执行结束之后,其活动对象并不会被销毁,因为内部匿名函数的作用域链仍然在引用这个活动对象。换句话说,当外部函数返回后,其执行环境的作用域链被销毁,但它的活动对象仍然会留在内存中被内部匿名对象所引用,直到内部匿名对象被销毁后,外部函数的活动对象才会被销毁。(如需要再补上作用域链关系图)

但需要注意的是:

  • 闭包只能取得外部函数中任何变量的最后一个值,例子如下代码所示;
  • 内部匿名函数的执行环境具有全局性,因此其this对象通常指向window,例子如下代码所示;
  • 内存泄露,指的是闭包在某些IE版本中会发生一些特殊问题,如果闭包的作用域链中保存着一些HTML元素,那么就意味着该元素无法被销毁,例子如下代码所示

//第一个问题
function createFunction () {
    var result = new Array();
    for (var i=0; i<10; i++) {
        result[i] = function() {
            return i;
        };
    }
        return result;        //实际上result总是返回10
}

//第二个问题
var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function() {
        return function () {
            return this.name;
        }
    }
};
alert(object.getNameFunc()());    //在非严格模式下,输出“The Window"

//第三个问题
function assignHandler () {
    var element = docunment.getElementById("someElement");
    element.onclick = function() {
        alert(element.id);    //只要此匿名函数存在,element的引用数至少是1,它占用的内存永远不会被回收。
    };
}

那么,闭包会带来占用内存的问题吗?

  分配给浏览器的内存通常要比分配给桌面程序的少,这样做的目的也是出于安全考虑,防止运行javascript时网页耗尽系统内存而导致系统崩溃。内存限制不仅会影响给变量分配内存,同时也会影响调用栈以及在一个线程能够同时执行的语句的数量,所以确保占用更少的内存可以让页面的性能更优。优化内存占用最佳的方式是为执行中的代码只保存必要的数据,一旦数据不再有用,最好将其值设置为NULL来释放引用;这一做法适用于大多数全局变量和全局对象。局部变量会在离开执行环境时自动解除引用。

  而创建闭包必须要维护额外的作用域,因此过度使用闭包可能会占用大量内存。

所以闭包该怎么写?

//针对第一个问题
function createFunction () {
    var result = new Array();
    for (var i=0; i<10; i++) {
        result[i] = function(num) {  //函数参数是按值传递的,会将i的当前值复制给num
            return function(){       //创建并返回了一个访问num的闭包
                return num;
            };
        }(i);
    }
    return result;    //这样,result中的每个函数都有num变量的一个副本,因此就可以各自返回不同数值
}

//针对第二个问题
var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function() {
        var that = this;        //在定义匿名函数之前把this对象赋值给了that变量
        return function() {
            return that.name;   //定义了闭包之后,闭包是不是仍然可以访问这个变量?所以即使在函数返回之后,that仍然引用着object
        };
    }
};
alert(object.getNameFunc()()); // 所以成功返回"My Oject"

//针对第三个问题
function assignHandler () {
    var element = docunment.getElementById("someElement");
    var id = element.id;        //在闭包引用该变量之前消除了循环引用,但还不能消除内存泄漏的问题
    element.onclick = function() {
        alert(element.id);
    };
    element = null;    //闭包会引用外部函数的整个活动对象,其中包含着element,即使闭包不直接引用着element,外部函数的活动对象中也仍然会保存一个引用,因此,有必要把element设置为null,这样就能解除对DOM对象的引用。减少引用数,得以回收内存。
}

简单的说,javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。