javascript中代码经常用来封装代码,好的闭包使用能使代码清晰明了,起到意想不到的功效,但是如果使用不当,闭包就很容易因其内存泄露,这篇文章,主要对闭包的如何形成及函数的作用域链做一个简单描述。
我们先来看wiki上对于闭包(closure)的解释,闭包是词法闭(lexical closure)包的简称,是引用了自由变量的函数。这个被引用的自由变量和函数一起存在,既然已经离开了创建它的环境也不例外。因此,另一种说法认为闭包是由函数和其相关的引用环境组合成的实体。
在一些语言中,在函数中定义了另一个函数时,如果内部的函数引用了外部的函数,则可能产生闭包。在外部函数运行时,就产生了闭包,闭包中包含了内部函数,以及所需外部函数中变量的引用(这个变量即是我们前面说的自由变量),其中的引用成为上值(upvalue)。
以上的定义时维基百科中对闭包的定义,其中有关于javascript中闭包形成的一个例子,这个解释比javascript中闭包官方的解释简单易懂得多了。
我们来看一个实例
function createIdFunc(){
var id=0;
return function(){
return id++;
}
}
函数createIdFunc的返回值也会一个函数。当createIdFunc被调用时,会形成一个闭包,并把这个闭包返回。这个闭包就是内部的函数以及他所引用的自由变量id。
我们来看一下这个函数的调用:
var getId = createIdFunc();
var id0 = getId();
var id1 = getId();
var id2 = getId();
我们执行这几行代码,可以看到id0的值为0,而id1的值为1,id2的值为2,如果继续执行getId()函数,我们会发现它的返回值会依次递增,也就是闭包中所引用的自由变量一直在递增,闭包保留了自由变量的状态。
为什么呢?这就是我们刚才说到的createIdFunc被调用时,形成了一个闭包,就是createIdFunc中定义的内部函数以及内部函数引用的自由变量,这个闭包以函数的形式返回,被我们赋值给getId,并一直存在,直到getId被释放掉。因此每次调用getId,我们可以认为对这个闭包的调用(这个说法并不正确,欢迎大家给出一个合适并易于理解的描述),而这个闭包中的自由变量id并没有随着createIdFunc调用的结束而被释放,每次的调用都会造成闭包中自由变量id的递增。
注:我们删除了函数getId时,闭包占用的内存并未释放,因此可以证明闭包并未删除。
delete getId; //delete function getId, but the closure is still exist
我们再来看另一个例子:
function func(){
var i = 0;
alert(i);
return function(){
alert(++i);
}
}
varfunc1 = func();
func1();
func1();
var func2 = func();
func2();
func2();
func1();
func1();
func2();
你先想一下,这段代码被执行之后,浏览器会弹出什么样的值呢?如果你真的理解了闭包,你就能准确的推测出结果。我们看下结果吧:
0//调用func时,弹出了0,并形成了闭包1,这个闭包依存于func1,同时包含一个自由变量0
1//调用func1,弹出1,闭包中的自由变量保存了状态,因此递增时是在原来值0的基础上,此时值为1
2//调用func1,弹出2,原来自由变量值为1,原因与上边一样,执行后值为2
0//调用func,弹出0,并形成一个新的闭包2,函数被付给了func2,同样包含自由变量值为0,现在已经有两个闭包了
1//调用func2,弹出1,闭包2中的自由变量递增,此时值为1
2//调用func2,弹出2,闭包2中的自由变量递增,此时值为2
3//调用func1,弹出3,func1使用了闭包1中的自由变量,原来值为2,计算后为3
4//调用func1,弹出4,同样是因为闭包1的缘故
3//调用func2,弹出3,调用func2,关联的闭包2中的自由变量上次计算后的值为2,此次计算后为3
与你预想的是否一样呢?从上边的例子,我们至少可以看出:
1)每次调用func都会形成一个闭包,第一次是func1与自由变量构成了一个闭包,第二次是func2与自由变量构成了闭包;
2)每次外部函数执行结束后,其局部变量已经释放,每次调用func,弹出的都是0,局部变量重新初始化
3)闭包中使用的自由变量其实是在外部函数执行时,被拷贝了一个副本,保存于闭包当中,与外部函数(这个示例中就是func)已经没有关系,示例中func1和func2闭包中自由变量值的变化是互不影响的。
我们再来看作用域链的定义及形成(此部分完全引自李松峰翻译的理解javascript闭包,详细内容可以查看原译文及英文,因此下边部分我这里只是简单介绍,原文描述的非常清晰)
Javascript代码都运行在一个执行环境中。当一个javascript函数被执行时,就会进入这个函数的执行环境中,如果其中调用了另一个函数,则会创建一个新的执行环境,保存当前的执行环境,并进入新的执行环境,直到被调用的函数执行完成,则会返回保存的原始执行环境。在此过程中,运行中的javascript代码构成了一个执行环境中,与大多数的语言中的函数调用一样。
在创建执行环境的过程中,会按照先后顺序,完成一系列的操作,首先会创建一个活动对象,活动对象是一种拥有可访问的命名属性,但没有prototype且不能被javascript代码直接引用的特殊对象;其次,会创建arguments对象,然后把arguments对象的引用赋值给活动对象的arguments属性(关于活动对象,arguments对象,大家可以自行查阅ECMA规范描述,及相关资料,此处很难叙述清楚);然后,为执行环境分配作用域,作用域由对象列表组成,每一个函数都有一个内部的[scope]对象,这个对象也由对象列表组成。指定给一个函数执行环境的作用域,由函数所用引用的[scope]对象列表组成,活动对象也会被添加到该对象列表的顶端;再之后,会执行可变对象的变量实例化过程,此时使用活动对象作为可变对象,会将函数的参数作为可变对象的命名属性,然后将函数的局部变量(包括函数)也作为可变对象的命名属性;最后为this关键字赋值。
执行环境调用函数时创建的执行环境包含一个作用域链,由执行环境的活动对象添加到到执行环境所调用函数的[scope]对象列表的顶端组成的。