前言
在之前我们的文章中已经对js中的执行机制有了初步的认识,并解决了为什么 var 会导致变量声明提升的问题, 今天我们就要深入认识并理解js初步学习过程中的一个公认的难啃的骨头------------闭包 ,而再提及闭包之前先让我们先复习一下作用域和解决声明提升问题并了解作用域链这一概念
1.作用域
概念:变量/函数可以被访问的区域
特点:外层作用域无法直接访问内层作用域
作用域分为三大类:全局作用域,块级作用域,函数作用域,即我们在编译或理解代码可以根据此分类来将代码以此分为三大块来便于我们理解,下面就让我们通过代码来理解他们的特征
全局作用域
概念:代码的任何地方都能访问的作用域在全局下,以及在函数外层的区域
js
let a = 1
const b = 2
a = 3
function varTest(){
var x = 1
if(true){
let x = 2
console.log(x);
}
console.log(x);
}
varTest()
//输出
//PS C:\Users\ZZZ\Desktop\code> node 1.js
//2
//1
根据上述定义分析代码,变量a,b函数varTest都是在全局作用域下,他们都能够在代码的任何地方被使用
函数作用域
概念:在函数体内部的区域
js
function foo(){
var a = 1;
console.log(2);
}
foo()
console.log(a);
上述代码运行会出现报错a is not defined现象,这就是因为我们把变量a定义在了函数foo(){}作用于下而作用域的规则是内层可以访问外层而外层不能访问内层,所以在全局想要打印变量a时,找不到变量a从而报错
块级作用域
let+{}构成的结构
2.解决声明提升
根据上述我们对作用域的具体介绍让我们知道声明提升出现在var定义变量中,来让我们一起分析一下下面代码
js
function varTest(){
var x = 1
if(true){
let x = 2
console.log(x);
}
console.log(x);
}
varTest()
让我们根据画图来一起分析一下上面的代码,在全局作用域中我们调用了varTest()函数那我们来画一份varTest()的执行上下文首先在函数中var先声明了一个x变量,我们将其存入环境变量中赋予undefined值,之后我们发现在该函数中let又定义了一个x变量,那这个let所定义的x该如何处理呢?其实这就涉及到了我们上文所说的块级作用域下,在let定义x时出现了let+{}构成了块级作用域会存储在词法环境中则该函数的执行上下文该为如下所示:
则该代码运行过程:在函数中定义了x赋值为1,然后在块级作用域下定义x赋值为2,之后执行块级作用域下的x的输出2,之后该块级作用域结束第二个输出x为1,则输出结果为:2,1
能力提升
让我们再看一份代码
js
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
foo()
让我们一起来分析上面的代码:这串代码在全局作用域中调用函数foo(),那我们开始执行foo()函数的内容画它的执行上下文,首先,先定义了变量a在环境变量中,然后let b=2{}出现了let+{}形成一个块级作用域放在词法环境中,紧接着又出现let+{}构成块级作用域放入词法环境中,而用var定义c没有构成块级作用域则c就是个环境变量,最后d也是作为块级作用域放入词法环境中,则执行上下文如图书所示: 那我们从上到下开始执行函数中的语句先输出a,那我们先从词法环境中去找,并且从词法环境中维护的栈顶开始往下找。如果没找到就去变量环境中找。则输出1.
然后输出b那我们就要在词法环境中找发现词法环境中有两个b由于是栈的形式始终遵循先进后出规则从上上到下来读取值则,输出值为3
此时注意我们块级作用栈的上下文已经执行结束,而我们之前提及过在调用栈里,当一个函数执行上下文执行完毕时,它是会被销毁的。则第一个块级作用域就要被销毁,那现在foo的执行上下文该如下所示:
继续执行代码输出b的值,综上所述,输出2,则该块级执行上下文销毁,在就输出c的值为4,最后输出d的值,而在变量环境中没有变量d,则会报错,让我们看一下在vscode运行下的结果吧
3.作用域链
上面我们已经回顾了什么是作用域,以此为基础让我们来聊一聊作用域链吧,其实作用域链就是就是各种作用域之间的嵌套关系。让我们通过一份代码来更好的理解这一概念吧
js
function bar(){
console.log(myname);
}
function foo(){
var myname = '管总'
bar()
console.log(myname);
}
var myname ='zyx'
foo()
我们可以先想一下这份代码运行输出结果是什么呢?让我们来画出整个代码的执行上下文来进行分析吧: 首先我们会有一份执行全局上下文开始对全局进行编译,在全局变量环境中定义了myname,函数foo()和bar(),并在全局下myname赋值为'zyx',再继续执行调用foo()函数,开始生成一份foo()执行上下文,在对foo()的代码进行编译其中定义myname赋值为管总,再继续执行调用了bar()函数,生成一份bar()函数的执行上下文在对bar()代码进行编译。
那我们在运行过程中调用bar()函数输出myname在函数内并没有找到myname变量那到底应该去哪去寻找呢?他会是去找foo()执行上下文中的myname还是全局作用域下的myname呢?为解决这个问题我们要提出一个新的概念词法作用域 也叫词法环境,词法作用域其实就是指的这个函数定义在了哪个域中,这个域就叫该函数的词法作用域,而我们上面代码的函数foo()以及bar()都定义在全局下,那他们的词法作用域就是全局作用域,而在每个执行上下文中,其实存在着这么一个指向outer,它就指向的是这个执行上下文所在的词法作用域,当在自己的执行上下文中没找到变量值时它就会去所在的词法作用域中找。如图所示:
那代码执行就会出现,在执行bar()函数时在其函数作用域中并没有找到myname那就要到其词法作用域了去找,对于函数bar()函数其词法作用域就是全局作用域则myname输出就是zyx,bar()函数执行结束,该函数作用域就会销毁,来继续执行foo()函数则在foo()中的输出就是'管总',让我们来开一下输出结果
4.闭包
概念:有权访问另一个函数作用域中的变量的函数;一般情况就是在一个函数中包含另一个函数。 从官方定义我们知道闭包是一个函数,只不过这个函数有超能力,可以访问到另一个函数的作用域。 上面我们已经对作用域连的概念有了一定了解,下面让我们根据一段代码来进一步理解闭包的概念
js
function foo(){
function bar(){
var a = 1
console.log(b);
}
var b = 2
return bar
}
const baz = foo()
baz()
根据上面代码我们调用baz()实际上就是运行foo()函数的返回值即执行bar()函数,但我们在前面介绍了在一个函数运行结束后他的作用栈会自动销毁,那我们再去执行bar()输出的b应该是找不到,可是此代码输出结果为2这是为什么呢?其实上面代码的执行上下文如下所示:
根据上面的图片中我们可以看到foo()在销毁之后旁边出现了一个区域用来储存b = 2,而这个b正是函数foo当中的变量b。当执行函数bar的时候由于在函数内部找不到变量b,所以bar内部的outer会往词法作用域查找变量,而词法作用域已经被销毁了。这时候就得用到闭包了,js官方为了弥补这一缺点,设置了一片区域用来储存需要用到的变量来方便调用,这时候outer便会去这片区域查找变量b,而这正是闭包。
闭包表现形式:
第一,闭包是一个函数,而且存在于另一个函数当中
第二,闭包可以访问到与该函数的父级函数的变量,且该变量不会销毁
闭包的优缺点
优点:
- 保护变量:闭包可以起到封装变量的作用,避免变量被外部意外修改。 延长变量寿命:即使外部函数执行完毕,闭包仍然可以访问外部函数作用域中的变量,延长了变量的生命周期。
- 实现模块化:闭包可以模拟私有方法和属性,帮助我们实现模块化的编程结构。
缺点:
- 内存泄漏:如果闭包被错误使用,可能会导致内存泄漏,因为闭包会使得包含该闭包的函数中的变量无法被垃圾回收。
- 性能消耗:闭包会对内存产生额外的消耗,可能会影响性能,尤其在闭包嵌套过深的情况下。
闭包的应用
我们可以实现在避免全局命名冲突情况下的累加器功能实现
js
function add(){
let num = 0
return function foo(){
console.log(++num);
}
}
const res = add()
res()
res()
res()
//输出
//1
//2
//3
我们利用闭包的优点实现当我们每次调用res()都会实现++的操作