在介绍闭包之前,可以先聊聊函数在JavaScript中的地位,因为闭包的存在是与函数息息相关的。
闭包的概念出现于60年代,最早实现闭包的程序是Scheme,那么就可以理解为什么JavaScript中有闭包了,因为JavaScript中大量的设计是源自于Scheme的。而在不同的地方对JavaScript闭包的定义是不一样的,但是整体核心还是一致的,只是用不同的话来描述JavaScript闭包,以下是摘抄自三个地方的定义。
维基百科中对闭包的定义:
MDN中对闭包定义:
《JavaScript高级程序设计》中对闭包的定义:
对闭包定义的总结:
看了一大堆闭包的定义,那么到底什么情况下就形成了闭包呢?
(1)产生闭包的条件:简单来说,满足以下几个条件就可以说产生了闭包。
(2)常见的闭包。
将一个函数作为另一个函数返回值,例如:
function foo() { var name = 'foo' return function bar() { console.log(name) }}var fn = foo()fn()将一个函数作为实参传递另一个函数,例如:
function showDelay(msg) { setTimeout(function() { console.log(msg) })}showDelay('我形成了闭包')下面介绍闭包在访问和执行过程中的内存表现,进一步深入对闭包的了解,以如下代码为例:
示例代码:
function foo() { var name = 'foo' return function bar() { console.log(name) }}var fn = foo()fn()首先,在执行全局代码之前,会在内存中创建一个全局对象(GO),将全局执行上下文压入栈中,这时的fn还未被赋值;

当执行到var fn = foo()时,在调用foo之前创建foo的活动对象(AO),创建foo函数执行上下文,并将其压入栈中,接着执行foo函数,执行完成后fn指向bar函数内存地址;

foo函数执行完成后,foo函数执行上下文会弹出栈,而按道理foo的活动对象(AO)是需要被销毁的,那到底有没有销毁,我们接着看;
接着执行fn(),因为fn是指向bar函数的,执行之前会先创建bar的活动对象(AO),然后执行console.log(name),而name会先去自己的AO中查找,发现没有找到就会去到上层作用域(父级作用域)中查找,最终找到foo并打印,这里bar函数的上层作用域就是foo函数的作用域对应foo的活动对象(AO);

bar函数执行完成后,bar函数的执行上下文弹出栈,对应bar的活跃对象(AO)被销毁,而foo的活跃对象(AO)还一直存留在内存中;

但是bar函数的父级作用域是在什么时候确定的呢?
根据上面闭包的访问和执行过程结合闭包的定义做一个总结:
在维基百科中定义的“闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)”,以及MDN中定义的“一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)”,其函数和关联的环境、函数和对其周围状态的引用,对应的就是上面的bar函数和上层作用域中的name;

而对于维基百科中提到的“闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行”,也就是在捕捉bar函数时,同时捕捉到对name这个自由变量的引用,执行完foo函数后,将bar函数赋值给fn,最后执行fn时,也是能正常访问到name的;
了解了其访问执行过程后,可以发现本应该被销毁的foo的活跃对象(AO),在代码执行完后最终没能被销毁,而这样的情况称之为内存泄露,下面就来谈谈闭包的内存泄露;
如果对上面的执行过程不清楚,可以先看看这篇文章:JavaScript的执行过程(深入执行上下文、GO、AO、VO和VE等概念)
闭包会保留它们包含函数的作用域,所以比其它函数更占用内存,而过渡使用闭包可能导致内存过度占用,也就是内存泄露,而除了内存泄露这个概念还有一个内存溢出的概念,下面就先了解一下这两者的区别和关系:
那具体怎么解决闭包产生的内存泄露呢?
fn = null;闭包其实是可以在浏览器中观察到的,在查看闭包之前先来讨论一个问题,外层函数的活跃对象(AO)不会被销毁,是不是里面所有的属性都不会被销毁呢?
如果将上面的代码改成下面这样,多增加两个变量age和message,但是bar函数中并没有对age和message有引用:
function foo() { var name = 'foo' var age = 18 var message = 'hello bibao' return function bar() { console.log(name) }}var fn = foo()fn()形成闭包后,name是一定不会被销毁的,这个上面已经验证过了;
具体age和message有没有被销毁,可以在代码中打上断点,在Chrome浏览器查看对应的闭包;

观察上面的结果是没有age和message属性的,这个就涉及到JS引擎的实现了,像V8引擎就对其进行了优化,对于闭包内层函数没有使用到的自由变量,是不会被保存的,这样就大大提升了内存的使用率;