对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但是需要付出非常多的努力和牺牲才能理解这个概念。
——《你不知道的JavaScript》
在JavaScript中的”神兽“,很多小伙伴会觉得闭包这玩意太恶心了,怎么着都理解不了...其实刚接触JavaScript的时候我也是这样。
但是!!!闭包真的非常重要!非常重要!非常重要!重要的事情说三遍!!!
接下来,我会带着大家真正意义上的理解闭包。
目录
1、作用域
2、作用域链
2.1 概念
三、解释闭包
4、模块输出
一 、闭包概念描述
《JavaScript权威指南》这样描述:
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这就是叫闭包。
《你不知道的JavaScript》这样描述:
闭包是基于词法作用域书写代码时所产生的自然结果。
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
从以上的描述。要真正理解闭包概念,要先深刻理解以下几个知识点,可以称为闭包前置知识点
二 、闭包前置知识点
1、作用域
《你不知道的JavaScript》这样描述:
作用域可以理解为一套规则,来定义变量存储在哪里,使用的时候怎么找到他们。
作用域是负责收集并维护由变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对变量的访问权限。
而我是这么理解的:作用域就是一个独立的对象,里面存储了变量,在对象中定义了一系列的规则,来限制外部访问里面的变量,来区分变量让不同作用域下同名变量不会有冲突。
如下图所示,红框区域就是一个作用域
2、作用域链
2.1 概念
作用域链可以理解为一个全局对象。在不包含嵌套的函数体,作用域链上有两个对象,第一个定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。
举个栗子,如图所示这是不包含嵌套的函数体的作用域链
举个栗子,如图所示这是包含嵌套的函数体的作用域链
2.2 使用规则
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
举个栗子,如图所示
比如要在作用域3中查找变量a的值,但是发现作用域3中没有变量a,就去作用域2中找,发现了变量a就停止了查找。
注意:在查找过程中不会跑去作用域4中查找。因为作用域4不是作用域3的外层嵌套作用域。
2.3 创建规则
理解作用域链的创建规则对理解闭包是非常重要的
首先我们定义一个函数的时候,开始就创建并保存了一条作用域链,里面包含一个全局作用域对象,当函数被调用时,会创建一个新对象(作用域)来存储它的变量,并将这个对象添加到开始创建的作用域链上,同时创建一条新的表示调用函数的作用域的“链”。
仔细琢磨一下下面代码,就可以理解。
function foo(a){
let b = a*3;
function bar (c){
console.log(a,b,c)
}
bar(b*2)
}
foo(2);//2,6,12
foo(3);//3,9,18
3、词法作用域
词法作用域是作用域的一个工作模型。
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你写代码时将变量和块作用域写在哪里来决定的。
举个栗子,如下图我把let b = a *3
写在foo(){...}这个函数作用域中,那么变量b的作用域就是foo(){...}这个函数作用域。
三、解释闭包
下面以一个非常典型的闭包例子来解释闭包。
function foo(){
let a = 2;
function bao(){
console.log(a)
}
return bao
}
let bar=foo();
bar();
上面代码中,闭包是哪个,是foo(){...}
,还是bao(){...}
。用Chrome断点调试一下就知道。
闭包是foo(){...}
这个函数,再看一下计算机科学文献是怎么定义闭包的。
这个术语非常古老,是指函数中的变量可以被隐藏在作用域之内,因此看起来是函数将变量“包裹”起来。
上面foo(){...}
将变量a隐藏在它的作用域内,从代码上看把变量a包含在函数内。
看到这里你也许会这么想,为什么下面的函数pyh不是闭包,它也把变量b包含在函数内。
function pyh(){
let b = 2;
console.log(b)
}
再读一下《JavaScript权威指南》中怎么表述闭包
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这就是叫闭包。
注意上面说函数内的变量可以保存在函数作用域内,那么pyh函数内的变量b可以保存在pyh(){...}
这个函数作用域内。
显然不能的,因为pyh函数执行后,pyh(){...}
这个作用域会被销毁,自然变量b就不存在,不会被保存。
浏览器垃圾回收策略,规定如果一个对象没有被引用,会被当垃圾一样回收并销毁。
那怎么不让pyh(){...}
作用域不会销毁,很简单,作用域也是个对象,让它被引用,就不会被销毁,这样作用域中的变量b自然也得以保存,这样就实现闭包。
怎么让它被引用?可以通过作用域链来实现。正如上面所说函数对象可以通过作用域链相互关联起来。
对pyh函数进行改造一下
function pyh(){
let b = 2;
function bao(){
console.log(b)
}
bao();
}
复制代码
我们用作用域的创建规则来讲解一下pyh(){...}
作用域怎么被引用。
pyh函数定义时创建了一条作用域链A,调用时将pyh(){...}
作用域添加到作用域链A上,bao函数定义时创建了一条作用域链B,调用时将bao(){...}
作用域添加到作用域链B,同时创建一条表示函数调用作用域的“链”,将作用域链A和作用域链B连在一起,相当pyh(){...}
作用域嵌套了bao(){...}
作用域。
这时候,bao函数中的console.log(b)
执行时会去寻找变量b,发现bao(){...}
作用域中没有,就会根据作用域链的使用规则去pyh(){...}
作用域中寻找,找到后使用其中的变量b,这就对pyh(){...}
作用域进行引用,导致pyh函数调用后pyh(){...}
作用域本来是会被销毁,但是它被bao函数引用了,导致无法销毁得以保存,自然作用域中的变量b也得以保存,这时pyh函数就变成了闭包。
当然pyh函数不是一个完整的闭包,它只运用到闭包规则的一部分,这部分是闭包规则的核心,非常重要。
返回最上面那个典型的闭包例子foo函数,给大家解释一下foo函数怎么形成闭包。
在foo函数外部定义变量bar来存储foo函数返回的结果。foo函数定义时创建一条作用域链A,调用时foo(){...}
作用域被添加到作用域链A,bao函数定义时创建了一条作用域链B,调用时将bao(){...}
作用域添加到作用域链B,同时创建一条表示函数调用作用域的“链”,将作用域链A和作用域链B连在一起,相当foo(){...}
作用域嵌套了bao(){...}
作用域。
在foo函数执行完毕时候,返回bao函数,并赋值到外部变量bar上,当执行bar();
时,相当调用bao函数,bao函数中的console.log(a)
执行时会去寻找变量a,发现bao(){...}
作用域中没有,就会根据作用域链的使用规则去foo(){...}
作用域中寻找,找到后使用其中的变量a,这就对foo(){...}
作用域进行引用,导致foo函数调用后foo(){...}
作用域本来是会被销毁,但是它被bao函数引用了,导致无法销毁得以保存。
大家注意了,bao函数调用后bao(){...}
作用域会被销毁,这时候foo(){...}
作用域的引用就会消失,也会被销毁。但是,但是foo函数返回值是bao函数,被外部变量bar引用了,被赋值给外部变量bar,这就导致foo(){...}
作用域是无法销毁,那么作用域foo(){...}
中的变量a就可以得以保存。这是foo函数就形成了一个闭包,foo函数把变量a包裹起来。
理解闭包过程中,要切记一点,函数调用结束后,在函数定义时创建的作用域链式不会马上消失的。
四、再次解释闭包
上面是通过作用域链来解释闭包,大家看起来是不是云里雾里的。其实闭包没那么神秘,难以理解。
在《你不知道的JavaScript》中写的特别好。
JavaScript中闭包无处不在,你只需要能够识别并拥抱它,闭包是基于词法作用域书写代码时所产生的自然结果。
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
还是以一个非常典型的闭包例子来解释闭包。
function foo(){
let a = 2;
function bao(){
console.log(a)
}
return bao
}
let bar=foo();
bar();
首先,我们很清楚知道bao函数的内部作用域3能够访问bao函数的词法作用域2。
然后bao函数被当作foo函数的返回值。在foo函数执行后,其返回值(也就是内部的bao函数)赋值给变量bar并调用bar(),实际上是调用了内部的bao函数。这时bao函数在自己定义的词法作用域2以外的地方(作用域3)执行。
在foo函数执行后,其内部作用域2通常会被销毁,因为浏览器垃圾回收器会将不被引用的对象回收销毁。 事实上foo函数的内部作用域2依然存在,没有被回收。谁在引用这个内部作用域2?是bao函数中变量a在引用。
当变量bar被实际调用(调用内部bao函数),它可以访问bao函数定义时的词法作用域2,因此它可以访问变量a。这时bao函数在定义时的词法作用域2以外的地方被调用。仍然可以继续访问定义时的词法作用域。 这就是闭包。
按照《你不知道的JavaScript》中描述闭包可以这样描述:
当bao函数可以记住并访问所在的词法作用域2时,就产生了闭包(foo函数),bao函数不在词法作用域2中被调用仍然可以访问词法作用域2。
这样描述闭包是不是清楚了很多,不要特意去想如何实现闭包,闭包就是基于词法作用域书写代码时所产生的自然结果。
五、闭包的应用
1、私有化全局变量
说起闭包的作用,我不禁想起我第一次接触闭包的场景。那时在做个轮播图,需要一变量来存储点击按钮的次数,当时想都没想就在全局这么写
var prevCount = 0;
var nextCount = 0;
在后面审核代码时候,就挨训了,经理就问我一句话,如果其它地方的变量名跟这一样,那怎么办?你用闭包把这段代码重新改造一下。
接着就去看闭包,结果看的云里雾里的,只能再问经理。经理随口就说用立即执行函数。
<div id="prev">上一张</div>
<script>
(function() {
var prevCount = 0;
var nextCount = 0;
function prev() {
//轮播的代码
prevCount++;
console.log(prevCount)
}
$('#prev').click(prev)
})()
</script>
这样变量prevCount和变量nextCount就变成wheel私有的。
2、外部访问函数内部变量
众所周知,外部是访问不到函数内部的变量。
function foo(){
let a ='我是foo函数内部的变量a'
}
console.log(a);//Uncaught ReferenceError: a is not defined
那么怎么在外部访问到foo函数内部的变量a,闭包带你实现。
function foo(){
let a ='我是foo函数内部的变量a'
function bao(){
return a
}
return bao
}
let b=foo();
console.log(b());//我是foo函数内部的变量a
也许你会觉得这个没必要用的闭包,这样就行
function foo(){
let a ='我是foo函数内部的变量a'
return a
}
let b=foo();
console.log(b);//我是foo函数内部的变量a
那么如果你要修改foo函数内部的变量a呢?
function foo(){
let a ='我是foo函数内部的变量a'
function bao(c){
a = c
return a
}
return bao
}
let b=foo();
console.log(b('我修改了foo函数内部的变量a'));//我修改了foo函数内部的变量a
3、构建私有作用域
一个很经典的例子,就是for循环中闭包应用。
var arr=[]
for(var i = 0; i<10;i++){
arr[i]=function(){
console.log(i)
}
}
arr[6]()
上面arr[6]()
输出的是10,而不是6,那么要怎么做才输出6。
在块级作用域出现前,我们使用闭包构建私有作用域解决。
var arr=[]
for(var i = 0; i<10;i++){
(function(i){
arr[i]=function(){
console.log(i)
}
})(i)
}
arr[6]()
4、模块输出
function module() {
let n = 0;
function get(){
console.log(n)
}
function set(){
n++;
console.log(n)
}
return {
get:get,
set:set
}
}
let a = module();
let b = module();
a.get();//0
a.set();//1
b.get();//0
a和b都用于自己的私有作用域,互不影响
六、闭包的副作用
1、在函数中使用定时器,形成闭包,导致内存泄露
function foo(){
var a =1
setInterval(function(){
console.log(a)
},2000)
}
foo()
以上在foo函数中使用了定时器,是foo函数成为闭包,本来foo函数执行后变量a会被回收销毁,但是定时器中调用函数有引用到变量a,导致变量a无法被销毁一直存在内存中。应该使用个外部变量赋值定时器,以便停止。
let timer = null;
function foo(){
var a =1
timer=setInterval(function(){
console.log(a)
},2000)
}
foo();
clearInterval(timer)
2、闭包返回被外部变量引用,导致内存泄露
function foo() {
var a = 1
function bao() {
console.log(a)
}
return bao
}
let bar = foo();
bar();
bar = null;
以上在foo函数中返回了bao函数,foo函数又把执行结果,赋值给变量bar,执行bar()
,即是执行函数bao,而函数bao对变量a有引用,导致foo函数执行后变量a,不能释放,导致内存泄露。
可以通过将bar = null
,断开变量bar对变量a的引用,释放变量a。
如果想要更高效、更系统地学会javascript,最好采用边学边练的学习模式。
如果觉得javascript的学习难度较高,不易理解,建议采用视频的方式进行学习,推荐一套看过讲的很不错的视频教程,可点击以下链接观看:
https://www.bilibili.com/video/BV1Ft411N7R3
作者:红尘炼心