那些年,我们一起学过的闭包

如果你是一名前端,你一定会接触到闭包,自己学习过程中或者几乎每次面试过程中,都会涉及到这个概念,但是你真的掌握了吗?今天我们就来一起谈谈闭包。

什么是闭包?

先来一点专业的解释,闭包是 JavaScript 中的一项功能,其中内部函数可以访问外部(封闭)函数的变量。内部函数可以访问外部函数的变量这个现象就被称为闭包。

闭包有三大特点:

  1. 它可以访问自己的作用域
  2. 它可以访问外部函数的变量
  3. 它可以访问全局变量

对于外行来说,压根不能理解这样的现象,因为这看起来太玄学了。

那到底什么是真正的闭包?

让我们先来看看这个简单的闭包代码例子:

js 复制代码
function outsideFn() {
   const b = 10;
   function insideFn() {
      const a = 20; 
      console.log(a+b);
   }
   return insideFn;
}
const x = outsideFn();

我们有两个函数

  • outsideFn 函数是外部函数,它有变量 b ,并且在函数内部声明了内部函数 insideFn,并返回了 insideFn 函数
  • insideFn 则是内部函数,它有变量 a,在函数内部,它会去访问变量 b,最终打印出 a+b 的结果

从直观的角度来看,变量 b 的范围仅限于 outsideFn 函数,变量 a 的范围仅限于 insideFn 函数。

好,接下来,我们开始执行 outsideFn 函数,因为该函数有返回值,所以我们用一个变量 x 来存储返回结果。

js 复制代码
const x = outsideFn();

让我们逐步看看调用 outsideFn 函数时会发生什么:

  1. 初始化变量 b,并且将其值赋值为10,其可访问范围仅局限于 outsideFn 函数,
  2. 初始化 insideFn 函数,这个是一个函数声明,所以不会直接执行该函数,
  3. 最后,返回了 insideFn,它会去寻找名为 insideFn 的变量,结果发现是个函数,所以就直接返回了整个函数体,

这里要说明一点,return 语句不会直接执行内部函数,仅当后跟 () 时才执行函数

  1. return 语句返回的内容存储在变量 x 中。

按照上面的逻辑,x 的内容应该如下:

js 复制代码
function insideFn() {
   const a = 20; 
   console.log(a+b);
}
  1. outsideFn 函数执行完毕,其中的所有变量都将不复存在。

请注意,最后一步对于理解很重要。在 Javascript 中,一旦函数完成执行,在函数作用域内定义的任何变量都不再存在。也就是说,函数内部定义的变量的生命周期就是函数执行的生命周期。所以按道理来说,在 console.log(a+b) 中,变量 b 仅在 outsideFn 函数执行期间存在。一旦 outsideFn 函数执行完毕,变量 b 就不再存在,这很符合常识,对吧。

接下来,我们梅开二度,我们再次执行一次 outsideFn 函数,但是这次我们把结果保存在一个新的变量 y上面:

js 复制代码
const y = outsideFn();

执行完成之后,我们得到了 xy 两个函数,因为他们都是执行outsideFn 函数得到的结果。

这里先甩出一个问题,xy 是同一个函数吗?可以先思考一下,然后带着问题继续往下看。

现在,我们来执行一下 x 函数:

js 复制代码
x();

x 函数本质上就是 insideFn 函数,所以我们执行 x 函数其实就是执行 insideFn 函数。接下来我们一步一步分析一下insideFn 函数的执行过程:

  1. 初始化 变量 a ,并将其值设置为 20,其可访问范围仅局限于 insideFn 函数,
  2. JavaScript 现在尝试执行 a + b 。这就是事情变得有趣的地方。JavaScript 知道 a 存在,因为刚刚创建了它。但是,变量 b 不再存在。由于 b 是外部函数的一部分,因此 b 仅在 outsideFn 函数执行时存在。由于 outsideFn 函数早在我们调用 x 函数之前就执行完成了,因此 outsideFn 函数范围内的任何变量都不再存在。结果呢,我们会发现,成功的打印了 a+b 的值:

那么,JavaScript 是如何找到变量 b 的呢?

这,就是闭包

insideFn 函数可以访问 outsideFn 函数里面的变量,其实这里面是作用域链在起作用,简单来说,就是 insideFn 函数的作用域链里面,保存了insideFn 函数的作用域链:

在执行 a + b 时,先在 insideFn 函数的作用域里面寻找,结果没找到,接着就开始在 insideFn 函数的作用域链上面找,接着找到了outsideFn 函数的作用域,最终找到了 变量 b,最终成功执行 a + b

我们可以通过在 Google Chrome 上面打开命令行执行一下 insideFn 函数:

通过上面图片可以看到,insideFn[[Scopes]]属性,存在一个 Closure (outsideFn)属性,而在该属性里面,即可找到 { b: 10 }。而这,就是闭包。

现在让我们重新来看看闭包的特点:

  1. 它可以访问自己的作用域
  2. 它可以访问外部函数的变量
  3. 它可以访问全局变量

更进一步

你应该注意到,前文提到了执行了两次 outsideFn 函数,分别得到了 变量 x 和变量 y,那么 xy 是同一个函数吗?

js 复制代码
console.log(x===y);

不知道你回答正确了没有?事实上,xy 不是同一个函数,虽然他们是同一个函数体,但是 javascript 为他们分别创建了两个不同的内存区域。接下来,我们再来看看这部分代码:

js 复制代码
function outsideFn() {
   var b = 10;
   function insideFn() {
      var a = 20; 
       console.log("a= " + a + " b= " + b);
       a++;
       b++;
   }
   return insideFn;
}
const x = outsideFn();
const y = outsideFn();

x();
x();
x();
y();

我们在 insideFn 函数里面打印了一下 变量 a 和 变量 b 的值,接着分别执行 a++b++

注意,我们将 ab 都改成了 var 变量

我们分别调用三次 x 函数和一次 y 函数。好,现在开始无奖竞猜,最终的输出结果是什么?

结果是:

js 复制代码
a= 20 b= 10
a= 20 b= 11
a= 20 b= 12
a= 20 b= 10

答对了吗?让我们一步步检查这段代码,看看到底发生了什么。

第1次调用 outsideFn 函数:

  1. 初始化变量 b ,并将其设置为 10 (我们把 这个 b 暂时叫做 b1
  2. 返回 insideFn 函数,并将其保留在 变量 x 里面,这个时候 x 会保存insideFn 函数的作用域。
  3. outsideFn 函数执行完毕,其所有变量都不再存在。

第2次执行 outsideFn 函数:

  1. 初始化变量 b ,并将其设置为 10 (我们把 这个b 暂时叫做 b2
  2. 返回 insideFn 函数,并将其保留在 变量 y 里面,这个时候 y 会保存insideFn 函数的作用域。
  3. outsideFn 函数执行完毕,其所有变量都不再存在。

接下来开始执行:

js 复制代码
x();
x();
x();
y();

第1次执行 x 函数:

  1. 初始化变量 a ,并将其设置为 20
  2. 打印 变量 ab的值:
js 复制代码
a= 20 b= 10
  1. 变量 ab增加1
  2. x 函数执行完毕,其所有变量都不再存在。

x 函数执行完毕,其所有变量都不再存在。那么变量 a 就不存在了,但是变量 b 却仍然存在,因为 x 函数 还保留着 outsideFn 函数的作用域链。

第2次执行 x 函数:

  1. 初始化变量 a ,并将其设置为 20
  2. 打印 变量 ab的值:
js 复制代码
a= 20 b= 11

这里变量 b 就是取自作用域链,而我们第1次执行的时候,将变量 b 加1了,所以这里打印 b=11

  1. 变量 ab增加1
  2. x 函数执行完毕,其所有变量都不再存在。

第3次执行 x 函数:

  1. 初始化变量 a ,并将其设置为 20
  2. 打印 变量 ab的值:
js 复制代码
a= 20 b= 12

这里变量 b 就是取自作用域链,而我们第1次执行的时候,将变量 b 加1了,所以这里打印 b=12

  1. 变量 ab增加1
  2. x 函数执行完毕,其所有变量都不再存在。

这就是闭包的作用和实际表现,你现在理解了吗?

接下来我们再来看看 y 函数的执行:

  1. 初始化变量 a ,并将其设置为 20
  2. 打印 变量 ab的值:
js 复制代码
a= 20 b= 10
  1. 变量 ab增加1
  2. y 函数执行完毕,其所有变量都不再存在。

x 函数执行完毕,其所有变量都不再存在。那么变量 a 就不存在了,但是变量 b 却仍然存在,因为 x 函数 还保留着 outsideFn 函数的作用域链。

这个时候 变量 b 变成10了,也就是说,b1b2 不是同一个变量,这也印证了,xy 不是同一个函数。

最后

闭包真的是非常有趣的 javascript 特性,相信看完这篇文章,已经明白闭包的含义以及实际表现,希望大家有所收获。

最后我抛出一个延伸的问题:闭包有哪些实际应用呢?哈哈

相关推荐
你挚爱的强哥15 分钟前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森1 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js