对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但是需要付出非常多的努力和牺牲才能理解这个概念。

                                                                                               ——《你不知道的JavaScript》


在JavaScript中的”神兽“,很多小伙伴会觉得闭包这玩意太恶心了,怎么着都理解不了...其实刚接触JavaScript的时候我也是这样。

但是!!!闭包真的非常重要!非常重要!非常重要!重要的事情说三遍!!!

接下来,我会带着大家真正意义上的理解闭包。

目录

​一 、闭包概念描述​

​二 、闭包前置知识点​

​1、作用域​

​2、作用域链​

​2.1 概念​

​2.2 使用规则​

​2.3 创建规则​

​3、词法作用域​

​三、解释闭包​

​四、再次解释闭包​

​五、闭包的应用​

​1、私有化全局变量​

​2、外部访问函数内部变量​

​3、构建私有作用域​

​4、模块输出​

​六、闭包的副作用​

​1、在函数中使用定时器,形成闭包,导致内存泄露​

​2、闭包返回被外部变量引用,导致内存泄露​


一 、闭包概念描述

《JavaScript权威指南》这样描述:


函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这就是叫闭包。


《你不知道的JavaScript》这样描述:


闭包是基于词法作用域书写代码时所产生的自然结果。



当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。


从以上的描述。要真正理解闭包概念,要先深刻理解以下几个知识点,可以称为闭包前置知识点

二 、闭包前置知识点

1、作用域

《你不知道的JavaScript》这样描述:


作用域可以理解为一套规则,来定义变量存储在哪里,使用的时候怎么找到他们。



作用域是负责收集并维护由变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对变量的访问权限。


而我是这么理解的:作用域就是一个独立的对象,里面存储了变量,在对象中定义了一系列的规则,来限制外部访问里面的变量,来区分变量让不同作用域下同名变量不会有冲突。

如下图所示,红框区域就是一个作用域

重生!带你深入理解JavaScript的闭包_JavaScript

2、作用域链

2.1 概念

作用域链可以理解为一个全局对象。在不包含嵌套的函数体,作用域链上有两个对象,第一个定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。

举个栗子,如图所示这是不包含嵌套的函数体的作用域链

重生!带你深入理解JavaScript的闭包_Java教程_02

举个栗子,如图所示这是包含嵌套的函数体的作用域链

重生!带你深入理解JavaScript的闭包_闭包_03

2.2 使用规则

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

举个栗子,如图所示

重生!带你深入理解JavaScript的闭包_Java教程_04

比如要在作用域3中查找变量a的值,但是发现作用域3中没有变量a,就去作用域2中找,发现了变量a就停止了查找。


注意:在查找过程中不会跑去作用域4中查找。因为作用域4不是作用域3的外层嵌套作用域


2.3 创建规则

理解作用域链的创建规则对理解闭包是非常重要的

首先我们定义一个函数的时候,开始就创建并保存了一条作用域链,里面包含一个全局作用域对象,当函数被调用时,会创建一个新对象(作用域)来存储它的变量,并将这个对象添加到开始创建的作用域链上,同时创建一条新的表示调用函数的作用域的“链”。

重生!带你深入理解JavaScript的闭包_JavaScript教程_05

重生!带你深入理解JavaScript的闭包_JavaScript教程_06

重生!带你深入理解JavaScript的闭包_JavaScript_07

仔细琢磨一下下面代码,就可以理解。

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(){...}这个函数作用域。

重生!带你深入理解JavaScript的闭包_JavaScript_08

三、解释闭包

下面以一个非常典型的闭包例子来解释闭包。

function foo(){
let a = 2;
function bao(){
console.log(a)
}
return bao
}
let bar=foo();
bar();

上面代码中,闭包是哪个,是​​foo(){...}​​​,还是​​bao(){...}​​。用Chrome断点调试一下就知道。

重生!带你深入理解JavaScript的闭包_闭包_09

闭包是​​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函数就变成了闭包。

重生!带你深入理解JavaScript的闭包_Java教程_10

当然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的闭包_java_11

四、再次解释闭包

上面是通过作用域链来解释闭包,大家看起来是不是云里雾里的。其实闭包没那么神秘,难以理解。

在《你不知道的JavaScript》中写的特别好。


JavaScript中闭包无处不在,你只需要能够识别并拥抱它,闭包是基于词法作用域书写代码时所产生的自然结果。



当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。


还是以一个非常典型的闭包例子来解释闭包。

function foo(){
let a = 2;
function bao(){
console.log(a)
}
return bao
}
let bar=foo();
bar();

重生!带你深入理解JavaScript的闭包_JavaScript_12

首先,我们很清楚知道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



作者:红尘炼心