javascript中的闭包

  1. 简要介绍

  闭包可谓是js中的一大特点了,即便你对闭包没概念,你可能已经在不知不觉中使用到了闭包。闭包是什么,闭包就是一个函数能够访问到另外一个函数的变量。这就是闭包,解释起来就这么一句话,不明白?咱们来看一个简单的例子:javascript

function getName(){
    var name='wenzi';
    setTimeout(function(){
        console.log(name);
    }, 500);
}
getName();

  这就其实已是闭包了,setTimeout中的function是一个匿名函数,这个匿名函数里的name是geName()做用域中的变量,匿名函数里只有一个输出语句:console.log();html

  还有一个很经典的例子也能够帮助咱们理解什么是闭包:前端

function create(){
    var i=0;
    // 返回一个函数,暂且称之为函数A
    return function(){
        i++;
        console.log(i);
    }
}
var c = create(); // c是一个函数
c(); // 函数执行
c(); // 再次执行
c(); // 第三次执行

  在上面的例子中,create()返回的是一个函数,咱们暂且称之为函数A吧。在函数A中,有两条语句,一条是变量i自增(i++),一条是输出语句(console.log)。第一次执行执行c()时会产生什么样的结果?嗯,输出自增后的变量i,也就是输出1;那么第二次执行c()呢,对,会输出2;第三次执行c()时会输出3,依次累加。这个create()函数依然知足了咱们在刚开始时的定义,函数A使用到了另外一个函数create()中的变量i。java

  但是为何会产生这样的输出呢,为何i就能一直自增呢,create函数已经执行完并返回结果了呀,但是为何还能接着使用i呢,并且i还能自增。这里就涉及到了三个比较重要的概念,讲解完这三个概念,咱们对闭包就能够有一个比较好的理解了。程序员

  2. 三个重要概念

  本节的大部分解释来自于《JavaScript高级程序(第3版)》web

  2.1 执行环境与变量对象

  执行环境是JavaScript中一个重要的概念,它决定了变量或函数是否有权访问其余的数据,决定了它们各自的行为。每一个执行环境都有一个与之对应的变量对象,执行环境中定义的全部变量和函数都保存在这个对象中。虽然咱们的代码没法访问这个对象,可是解析器在处理数据时会在后台使用它。浏览器

咱们用一个比较简单的比喻来形容这两个概念。执行环境就是一我的,变量对象就是这我的的身份证号,每一个人都有其对应的身份证号。他这我的决定了他身上的不少属性和方法(动做),并且这我的的属性和方法都在他的身份证号上,当这我的消亡的时候,身份证号也就随之就注销了。安全

  全局执行环境是最外层的一个执行环境。在web浏览器中,全局执行环境被认为是window对象,由于全部的全局变量和全局函数都是做为window对象的属性和方法建立的。某个执行环境中的全部代码执行完毕后,该环境被销毁,保存在其中的全部变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或者浏览器——时才会被销毁),被垃圾回收机制回收。闭包

  每一个函数都有本身的执行环境。当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给以前的执行环境。函数

  2.2 做用域链

  做用域链是当代码在一个环境中执行时建立的,做用域链的用途就是要保证执行环境中能有效有序的访问全部变量和函数。做用域链的最前端始终都是当前执行的代码所在环境的变量对象,下一个变量对象是来自其父亲环境,再下一个变量对象是其父亲的父亲环境,直到全局执行环境。

  标识符解析是沿着做用域链一级一级地搜索标识符的过程。搜索过程始终从做用域链的前端开始,而后逐级地向后回溯,直到找到标识符为止(若是找不到标识符,一般会致使错误发生)。其实,通俗的理解就是:在本做用域内找不到变量或者函数,则在其父亲的做用域内寻找,再找不到则到父亲的父亲做用域内寻找,直到在全局的做用域内寻找!

  2.3 垃圾回收机制

  在js中有两种垃圾收集的方式:标记清除和引用计数。

  标记清除:垃圾收集器在运行时会给存储在内存中的全部变量都加上标记(具体的标记方式暂时就不清楚了),待变量已不被使用或者引用,去掉该标记或添加另外一种标记。最后,垃圾收集器完成内存清除工做,销毁那些已没法访问到的这些变量并回收他们所占用的空间。

  引用计数:通常来讲,引用计数的含义是跟踪记录每一个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数即是1,若是同一个值又被赋给另外一个变量,则该值的引用次数加1,相反,若是包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数为0时,说明没有办法访问到它了,于是能够将其占用的内存空间回收。

  除了一些极老版本的IE,目前市面上的JS引擎基本采用标记清除来除了垃圾回收。可是须要注意的是IE中的DOM因为机制问题,是采用了引用计数的方式,因此会有循环引用的问题,形成内存泄露。

var ele = document.getElementById(“element”);
var obj = new Object();
ele.obj = obj; // DOM元素ele的obj引用obj变量
obj.ele = ele; // obj变量的ele引用了DOM元素ele

   这样就形成了循环引用的问题,致使垃圾回收机制回收不了ele和obj。不过,能够在不使用ele和obj时,对这两个变量进行 null 赋值,而后垃圾回收机制就会回收它们了。

  3. 理解闭包

  在第2部分讲解了三个重要的概念,这三个概念有助于咱们更好的理解闭包。

  咱们再次拿出上面的这个例子:

function create(){
    var i=0;
    // 返回一个函数,暂且称之为函数A
    return function(){
        i++;
        console.log(i);
    }
}
var c = create(); // c是一个函数,即函数A
c(); // 函数执行
c(); // 再次执行
c(); // 第三次执行

   从上面的“每一个函数都有本身的执行环境”能够知道:create()函数是一个执行环境,函数A也是一个执行环境,且函数A的执行环境在create()的里面。这样就造成了一个做用域链:window->create->A。当执行c()时,函数A就会首先在当前执行环境中寻找变量i,但是没有找到,那么只能顺着做用域链向后找;OK,在create()的执行环境中找到了,那么就可使用了变量i了。

  但是咱们还有一个疑问,按照上面的说法,函数create()执行完毕后,这个函数与里面的变量和方法应该被销毁了呀,但是为何函数c()屡次执行时依然可以输出变量i呢。这就是闭包的独特之处。

  函数create()执行完毕后,虽然它的做用域链会被销毁,即再也不存在window->create这个链式关系,可是函数A()[c()]的做用域链还依然引用着create()的变量对象,还存在着window->create->A的链式关系,致使垃圾回收机制不能回收create()的变量对象,create()的变量对象仍然停留在内存中,直到函数A()[c()]被销毁后,create()的变量对象才会被销毁。

  所以,虽然create()已经执行完毕了,可是create()的变量对象并无被回收,还停留在内存中,依然可使用。

  从上面的讲解中咱们能够看到,闭包会携带包含它的函数的做用域,所以会比其余函数占用更多的内存。当页面中存在过多的闭包,或者闭包的嵌套不少很深时,会致使内存占用过多。所以,在这里建议:慎用闭包。

  4. 闭包与变量

  有不少新手为DOM元素绑定事件时,一般会这么写:

function bindClick(){
    var li = document.getElementsByTagName('li'); // 假设一共有5个li标签
    for(var i=0; i<li.length; i++){
        li[i].onClick = function(){
            console.log('click the '+i+' li tag');
        }
    }
}

   他的本意是想为每一个li标签绑定一个单独的事件,点击第几个li标签,就能输出几。但是,最后的结果倒是,点击哪一个li标签输出的都是5,这是为何呢?

  其实这位程序员写的bindClick()已经构成了一个闭包,下面的这个函数有他的做用域,而变量i本不属于这个函数的做用域,而是属于bindClick()中的:

// 匿名函数
function(){
    console.log('click the '+i+' li tag');
}

   所以,这就构成了一个含有闭包的做用域链:window->bindClick->匿名函数。但是这跟输出的i有关系么?有。做用域链中的每一个变量对象保存的是对变量和方法的引用,而不是保存这个变量的某一个值。当执行到匿名函数时,bindClick()其实已经执行完毕了,变量i的值就是5,此时每一个匿名函数都引用着同一个变量i。

  不过咱们稍微修改一下,以知足咱们的预期:

function bindClick(){
    var li = document.getElementsByTagName('li');
    for(var i=0; i<li.length; i++){
        li[i].onClick = (function(j){
            console.log('click the '+j+' li tag');
        })(i);
    }
}

   在这里,咱们使用当即执行的匿名函数来保证传入的值就是当前正在操做的变量i,而不是循环完成后的值。

   5. 闭包的应用场景

  (1)在内存中维持一个变量。好比前面讲的小例子,因为闭包,函数create()中的变量i会一直存在于内存中,所以每次执行c(),都会给变量i加1.

  (2)保护函数内的变量安全。仍是那个小例子,函数create()中的变量c只有内部的函数才能访问,而没法经过其余途径访问到,所以保护了变量c的安全。

  (3)实现面向对象中的对象。javascript并无提供类这样的机制,可是咱们能够经过闭包来模拟出类的机制,不一样的对象实例拥有独立的成员和状态。

  这里咱们看一个例子:

function Student(){
    var name = 'wenzi';

    return {
        setName : function(na){
            name = na;
        },

        getName : function(){
            return name;
        }
    }
}
var stu = new Student();
console.log(stu.name); // undefined
console.log(stu.getName()); // wenzi

   这就是一个用闭包实现的简单的类,里面的name属性是私有的,外部没法进行访问,只能经过setName和getName进行访问。

  固然,闭包还存在另一种形式,刚才咱们已经使用到了:

;(function(w){
    console.log(w);
})(window)

   之前也写过一次的闭包的文章,两个能够连着一块儿看:http://www.cnblogs.com/xumengxuan/p/3753713.html 

 

  本文地址:http://www.xiabingbao.com/javascript/2015/05/23/javascript-closure/

相关文章
相关标签/搜索