深入理解Javascript中的闭包
文本主要介绍闭包的原理以及常见用途。
前言
在正式介绍闭包之前,先介绍两个概念,这两个概念在学习闭包原理的时候非常有用:
- 函数在创建的时候除了函数对象本身会被创建以外还会创建一个作用域链对象,它是一个栈结构的对象,自底向上依次存储当前函数作用域链上函数的的活动对象。
- 函数在调用的时候才会创建活动对象,活动对象里包含了当前函数执行时自己局部的变量和函数,而这个活动对象就会在这时进入当前函数作用域链对象的栈结构里。
接着咱们来聊聊关于变量作用域的问题(此处不考虑let和const块级作用域):
- 全局变量:全局变量存在于整个程序的运行过程中,随处都可以访问,也就意味着随处都可以修改。
- 局部变量:只有在当前函数内部才可以访问的变量,别的地方不能访问也不能修改。
我们可以看出,全局变量是可以在声明之后重复使用的,但是也就意味着如果在某一处对他做了修改,那么别处也将应用这个修改,我们称之为“全局污染”。局部变量是不存在这个问题的,但是又不能重复使用,那么如果我们希望能有一种变量既可以重复使用又不会污染全局的话,就可以使用闭包。
什么是闭包?
以下是MDN 对于闭包的解释:
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
简单地说:闭包就是由嵌套函数形成的一个词法环境。
有了闭包,我们就可以在一个函数里访问另外一个函数的局部变量,这个变量可以重复使用且不会造成全局污染。
1 | function numCompany () { |
以上代码就是一个经典的闭包,一般构成闭包有三步:
- 外层函数嵌套内层函数
- 内层函数使用外层函数的局部变量(或参数)
- 将内层函数作为外层函数的返回值
然后我们就可以调用以上函数
1 | var fn = numCompany() |
这里的fn
接收的就是numCompany
的返回值也就是内层函数,所以fn
可以直接调用。而且反复调用fn
会把numCompany
的局部变量n
持续++
。也就是说,我们在fn
这个函数里反复操作了numCompany
的局部变量n
,这就是闭包。
由此可见,闭包可以:
- 在一个函数里访问另一个函数的局部变量,也就是访问另外一个作用域
- 变量即可以重复使用又不会污染全局(因为别的地方访问不到)
以上的例子我们可以用一个形象的比喻:外层numCompany
函数是银行大厅的一个取号机,机器里的getNum
是取号的算法,那么调用一次numCompany
就相当于安装了一台机器,然后就得到了取号的算法,以后有人来办理业务需要取号就只需要调用这个算法,得到的号码就是依次加一的。那么我们继续看下面的代码
1 | var fn = numCompany() |
如果我们调用两次外层函数,分别调用fn
和fn1
他们得到的结果是操作的同一个n
吗?结果明显不是,好比建设银行和招商银行分别安装了取号机,他们取到的号码会互相影响吗?明显不会。
每调用一次外层函数就得到一套闭包,多次调用时多个闭包,不会互相影响。
闭包的原理
为什么闭包可以实现这样的功能?接下来我们以上面的代码为例来看看闭包的形成过程。
1 | function numCompany () { |
一、预解析
预解析的时候变量提升,fn
还是undefined
,外层函数被声明,我们假设函数地址是0x1001
,这个时候除了函数本身被创建,还有他的作用域链对象也同时被创建了,不过此时只有栈底的一个window
对象,如下图:
二、调用时
当我们在调用外层函数numCompany
时,函数的活动对象此时被创建并且压栈到作用域链对象里,在numCompany
的活动对象里有一个局部函数getNum
以及局部变量n = 1
,getNum
在此时被创建,与此同时getNum
的作用域链对象也被创建,而他的作用域链对象的栈结构里已经有了window
和numCompany
的活动对象,如下图:
三、调用后
当numCompany
完成调用以后,numCompany
的活动对象就该出栈了,与此同时numCompany
的返回值(即getNum
)被fn
接收了,所以现在fn
不再是undefined
了,而是指向getNum
这个函数。
在正常情况下,一个函数调用完成以后活动对象出栈也就应该意味着活动对象应该被js的垃圾回收机制所回收,但是请看下图:getNum
、getNum
的作用域链对象以及numCompany
的活动对象三者之间的指向关系形成了一个闭环,我们称之为循环引用,循环引用也就意味着这三个对象是互相依赖的,他们谁也离不开谁,所以此时js并不会把其中任意一个对象进行释放操作,尤其内层函数getNum
还被全局变量fn
所依赖,因此他们都不会被释放,这其中对闭包起着决定性因素的就是外层函数numCompany
的活动对象不能被释放。这也是构成闭包最重要的原因,这样的话变量n
就一直存在在内存里不会释放,也就可以重复使用了。
四、fn调用时
外层函数调用结束就该调用fn
了,调用fn
实际上调用的就是getNum
,所以此时getNum
的活动对象会被创建并且入栈,由于getNum
并没有自己的局部变量或者函数,所以该对象是一个空对象。在执行++n
的时候就会沿着作用域链对象来查找变量n
,于是找到了numCompany
活动对象里的n
将其自增,此时我们就可以得到2。
五、fn调用后
fn
调用完成活动对象出栈并销毁。
以后再次调用fn
的话就是在调用一次getNum
,就再次重复上面的第四步和第五步,继续找到numCompany
活动对象里的n
继续自增,但是如果再调用一次外层函数numCompany
的话就会得到一套新的闭包,n
又会从1开始去自增。
闭包的特点
对于闭包我们需要记住以下几点:
- 构成闭包经典三步
- 外层函数嵌套内层函数
- 内层函数要使用外层函数的局部变量
- 内层函数作为外层函数的返回值
- 闭包的好处就是变量可以重复使用而且不会污染全局
- 闭包可以在一个作用域里访问另外一个作用域的局部变量
- 闭包的原理用一句话表示就是外层函数的活动对象不能被释放
- 但是,正是因为外层函数活动对象不能被释放,所以会占用过多的内存,并且有内存泄漏的风险(内存泄漏:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收)。
闭包的应用
一、点击li弹出下标
这是一道经典笔试题,其实可以有很多种解决方案,这里我们看看用闭包如何解决
1 | var list = document.querySelectorAll('li') |
在这个案例里我们每一趟循环都构成了一个闭包,li的点击事件的事件处理函数其实是内层函数,由外层函数把i作为实参传进来再用index来接收,这个index就是外层立即执行函数的活动对象不会被释放,所以每一个li的点击都是使用当前闭包的index,就不会一直都是length了。
二、单例模式
设计模式中的单例模式也是可以用闭包来实现的(懒汉单例)
1 | var getInstance = (function () { |
代码不是万能的,但不写代码是万万不能的
Dary记
-
更多干货,尽在公众号
转载请注明来源,文末有原始链接。欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 dary1112@foxmail.com
创作不易,您的打赏是我更新的动力
-
支付宝
-
微信
文章标题:深入理解Javascript中的闭包
文章字数:2.4k
本文作者:Dary
发布时间:2020-07-06, 21:47:00
最后更新:2020-07-13, 23:33:41
原始链接:http://www.xiongdalin.com/2020/07/06/closure/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。
Built By Dary