5

1. 简要介绍

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

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

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

还有一个很经典的例子也可以帮助我们理解什么是闭包:

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。

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

2. 三个重要概念

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。

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

/* 
    // 错误,onclick绑定的是立即执行函数的返回值,
    // 而此立即执行并没有返回值,也就是onclick = undefined
    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);
    }
} */
// 更正,onclick 绑定的是 function(){ console.log('click the '+j+' li tag'); }
for(var i=0; i<li.length; i++){
    li[i].onclick = (function(j){
        return function(){
            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进行访问。

当然,闭包还存在另外一种形式:

var a = (function(){
    var num = 0;
    return function(){
        return num++;
    }
})()
a(); // 0
a(); // 1
a(); // 2

本人个人博客:http://www.xiabingbao.com


小蚊酱
5.3k 声望208 粉丝

爱生活,爱前端