JavaScript 闭包
讲了好长时间的闭包前置知识,今天终于说到了闭包。正如前面文章中说到的,闭包涉及到的知识点很多,如果直接阅读本文,可能你会看不太懂,因此,为了更好地理解本文,建议你去看看前面几篇文章。
什么是闭包?
废话不多说,我们先来看看MDN对闭包的定义:
简单讲,闭包就是指有权访问另一个函数作用域中的变量的函数。
MDN 上面这么说:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
但是,在网上找了好多资料,也翻了好几本相关书籍,它们对闭包都有各种各样的定义。但是个人最认同的是《你不知道的JavaScript》中的描述:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
虽然其它的说法都没有错,但闭包应该是一种现象,你不用刻意去创建,因为闭包在代码中随处可见,只是你还不知道当时你写的那一段代码其实就产生了闭包。

闭包分析
上面已经说到了,当函数可以记住并访问函数所在的词法作用域,就产生了闭包。
看一段代码
1 | var scope = 'global scope' |
这段代码是不是很熟悉?没错,这就是我们上一篇文章JavaScript闭包之执行上下文最后着重分析的那段代码。但是仔细看看,这两段代码好像又有些不同,下面我们简要的分析下这段代码的执行过程。
- 执行全局代码,创建全局执行上下文。
1 | globalContext = { |
- 全局上下文压入执行上下文栈
1 | ECStack = [ |
- 全局执行上下文初始化
1 | globalContext = { |
- 初始化的同时,checkScope函数被创建
1 | checkScopeContext = { |
- checkScope函数上下文被压入执行上下文栈
1 | ECStack = [ |
- checkScope执行上下文初始化
1 | checkScopeContext = { |
- checkScope函数执行,返回foo函数的引用,checkScope从执行上下文栈中弹出
1 | ECStack = [ |
- 执行foo函数,创建foo函数执行上下文
1 | fooContext = { |
- foo函数执行上下文被压入执行栈
1 | ECStack = [ |
- foo函数执行上下文初始化
1 | fooContext = { |
- 执行foo函数,查找变量scope
1 | -'scope' |
- foo函数执行完毕,foo函数执行上下文从执行栈中弹出
1 | ECStack = [ |
看到这儿,小伙伴们应该思考一个问题:
当 foo函数执行的时候,checkscope函数上下文已经从执行上下文栈中被弹出了,怎么还会读取到checkscope作用域下的 scope值呢?
其实原因很简单,foo函数执行上下文维护了一个作用域链
1 | fooContext.Scope = [AO, checkScopeContext.AO, globalContext.VO] |
所以即使checkScope函数执行完毕,但是JavaScript依然会让checkScope.AO存活在内存中,foo函数依旧可以通过作用域链找到它。
还记得我们上面对闭包的定义吗?
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
我们再来看看foo函数,它是不是就记住了checkScope的作用域,因此上面的foo函数就是一个闭包。
闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。
闭包经典例子
在闭包中,有一个很经典的例子:
1 | for(var i = 0; i < 10; i++) { |
在这段代码中,我们对其预期的输出是0 ~ 9,但真正的输出结果却是10次10,这是因为当setTimeout中的匿名函数执行时,循环已经结束,此时全局上下文的VO:
1 | globalContext.VO = { |
究其原因:i是声明在全局作用域中的,定时器中的匿名函数也是执行在全局作用域中,那当然是每次都输出11了。
知道了原因,解决起来就简单多了,我们可以让i在每次循环的过程中都产生一个私有作用域
1 | for(var i = 0; i < 10; i++) { |
这样当setTimeout中的匿名函数执行时,全局上下文的VO:
1 | globalContext.VO = { |
和修改之前一样,完全没变化,但是此时setTimeout中的匿名函数的作用域链:
1 | Scope = [AO, 匿名函数Context.AO, globalContext.VO] |
而匿名函数的AO:
1 | 匿名函数Context.AO = { |
所以查找i的过程:
1 | -'i' |
闭包的应用
关于闭包的应用,小伙伴可以去看看这篇文章为了前端的深度-闭包概念与应用。
闭包的缺陷
- 闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
- 如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
说到了这儿,闭包基本上就全部过了一遍,在这个过程中,我翻了无数的书和博客,收获很大。这是我第一次写系列文章,感觉写的不怎么好,但这也算是一次经验吧!相信以后一定能写出好的文章。
本文完