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

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

什么是闭包?

先来一点专业的解释,闭包是 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 特性,相信看完这篇文章,已经明白闭包的含义以及实际表现,希望大家有所收获。

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

相关推荐
阿伟来咯~9 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端14 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱17 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai26 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨27 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
独行soc1 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试