目录
总结:1.当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
2.闭包是嵌套的内部函数,包含被引用变量(函数)的对象。可以在内部函数访问到外部函数作用域。
3.为什么不被垃圾回收清除?闭包所包含的整个作用域链所引用的 变量对象 (一些作用域中的变量的集合,包括局部变量对象和全局变量对象。)中的值不会被清除。
4.闭包问题的优化:可以使用自执行函数或者匿名函数来优化闭包。
上一篇复习了JavaScript中的作用域,接下来将注意力转移到JavaScript中一个非常重要但又难以掌握,近乎神化的概念上:闭包。
其实如果了解了上一篇的词法作用域,闭包的概念几乎是不言而喻的:闭包是基于词法作用域书写代码时所产生的自然结果。
我们甚至不需要有意识的创建闭包。闭包的创建和使用随处可见,而我们缺少的是根据自己的意愿来识别、拥抱和影响闭包的思维环境。
1.怎么产生闭包?
函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。听不懂不理解对吧,确实难以理解的,看下面这段话:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
简单来说,就是当一个内部嵌套函数引用了嵌套外部函数的变量或函数时,就产生了闭包。
下面这段代码来清晰展示闭包:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //2 --朋友,这就是闭包的效果
函数bar()的词法作用域能够访问foo()的内部作用域。在foo()执行后,其返回值(也就是内部的bar函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用了内部的函数bar()。
bar()显然可以正常被执行,但是此例中,它在自己定义的词法作用域以外的地方执行。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器来释放不再使用的内存空间。由于看上去foo()内部的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇之处”正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原理是bar()本身在使用。
bar()所声明的位置,拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
总结:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
现在,我们貌似有点理解了闭包,那闭包到底是什么?
2.闭包到底是什么?为什么使用?
闭包是嵌套的内部函数,包含被引用变量(函数)的对象。
可以在内部函数访问到外部函数作用域。
正如上面所说,知道什么是闭包,但我们更需要知道为什么使用闭包?
使用闭包,一可以读取函数中的变量,二可以将函数中的变量存储在内存中,保护变量不被污染。而正因闭包会把函数中的变量值存储在内存中,会对内存有消耗,所以不能滥用闭包,否则会影响网页性能,造成内存泄漏。当不需要使用闭包时,要及时释放内存,可将内层函数对象的变量赋值为null。
3.闭包的原理
上篇作用域我们知道,函数执行分成两个阶段(预编译阶段和执行阶段)。
- 在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
- 执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。
4.由一个问题深入理解作用域闭包
为什么闭包不会被垃圾回收清除?
首先,我要说,这个问题是个伪问题,不具有特异性。
因为闭包的产生是因为一个函数被当前函数作用域外部的变量引用了,除非外部的变量被释放,否则闭包当然不会被回收。不只是闭包,只要是仍处于被引用状态的堆内存数据,都不会被垃圾回收清除,根本没必要单独拿出闭包来说一下嘛。闭包所保存的,无非是一些存放在堆上的数据而已。有用就不会被清除,没用自然会清除,GC 对闭包做的,跟对其它内存做的事情没什么两样。
但是,我们要深入理解作用域闭包,还是要讨论一下闭包的产生和垃圾回收。
首先,还是要从之前的作用域链说起:
当一个函数定义的时候,会创建一个包含全局变量对象和所有包含函数(的)活动对象的作用域链,并将它保存在这个函数对象的[[Scope]]内部属性上(函数也是一个对象,包含函数体和其他一些可访问或不可访问的属性,比如name、length等)。还记得前面说的词法作用域吗?
当这个函数执行的时候,会创建一个执行上下文,将[[Scope]]复制到执行上下文中的作用域链。然后,创建当前的活动对象,推到作用域链顶端。此刻,函数执行过程中可访问到的作用域是:活动对象(当前执行作用域中的变量集合)和作用域链(函数有权访问的所有包含函数的局部变量对象和全局变量对象)。
注意:
- 作用域链是一个指向各级变量对象的指针列表,作用域链不清除,各级变量对象也不会清除。
- 无论子函数是否引用了外部作用域的变量,都会包含完整的作用域链,都存在内存占用问题。只是要不要把未引用外部变量的函数称作闭包,就见仁见智了,我们稍后会讨论闭包的定义问题。
那么回归问题,为什么闭包不会被垃圾回收清除呢?
准确的说,是闭包所包含的整个作用域链所引用的 变量对象 (一些作用域中的变量的集合。包括局部变量对象和全局变量对象。)中的值不会被清除。
function fn () {
let a = 1;
let b = 2;
return function () {
console.log(a);
}
}
let foo = fn();
foo();
fn() 返回了一个匿名函数,被全局变量 foo 引用。那么 foo 所应用的匿名函数就是一个闭包。
如果闭包函数被外部变量接收,那么这个存在于堆内存中的闭包函数对象就一直存在着(因为它被外部引用着,只要外部作用域不退出,就不会被垃圾回收清除)。那么这个函数对象上的 [[Scope]] 属性(即作用域链)自然就不会被清除,那么作用域链引用的所有层级包含函数的活动对象就不会被清除。
这也就意味着,所有返回函数的函数被接收之后,都有占用内存的闭包问题。
我们需要厘清的是,无论闭包的定义如何(是内部函数,还是被引用的外部函数),按照语言解释器的机制,无法被垃圾清除的是函数创建时的作用域链。
5.闭包问题及优化
闭包的缺点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。
在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
在IE中可能导致内存泄露,解决方法:
1、在退出函数之前将不使用的局部变量全部删除。在函数最后将打算删除的变量赋值为null
2、避免变量的循环赋值和引用
3、利用jQuery释放自身指定的所有事件处理程序可以使用自执行函数或者匿名函数来优化闭包
注意:自执行函数不是闭包,但是自执行函数可以达到闭包的效果,同时它也可以写成闭包