深入理解JS闭包(请多多点赞收藏)

前言

JS闭包,对于每一个前端而言都是一个绕不开的概念。本人学习之初,因为闭包这个概念而花费了大量的时间以及精力去理解这个概念。所以在这里,我打算写一篇文章来分享一下本人的学习心得以及我眼中的闭包

什么是闭包

先来看看百度百科对闭包的定义:

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

上面提到了局部变量,那什么又是局部变量呢? 在理解局部变量之前,我们需要先知道,一个函数的执行流程是什么样的。

执行上下文

执行上下文(execution context)是JavaScript中最重要的一个概念。执行上下文定义了变量或者函数有权访问的其他数据,决定了它们各自的行为。每个执行上下文都有一个与之关联的变量对象

全局执行上下文是最外围的一个执行上下文。根据ECMAScript所在的宿主环境的不同,该上下文也不同。在浏览器中,全局执行上下文是windows对象,在全局环境下声明的变量为全局变量,所有的全局变量和函数都是作为window对象的属性和方法创建的。某个执行上下文中的所有代码执行完毕后,该上下文被销毁,保存在其中的所有变量和函数也随之销毁(全局执行上下文知道应用程序退出——例如关闭浏览器或者网页时才会被销毁)

而每个函数也都有自己的执行上下文,当执行流进入一个函数时,函数的环境就会被推入一个环境栈当中。而在函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境。

注意:执行上下文,顾名思义,是在某段代码执行时所定义的,所以它是动态的。某个函数在不同的环境调用时,可能会产生不同的执行上下文

我们来看个例子:

// 全局环境
var a = "全局环境";

function A() {
  var a = "局部环境";
  B();
}

function B() {
  console.log(a);
}
A();

我们来模拟一下浏览器执行上述代码时的流程。

  1. 执行栈中推入全局执行上下文,全局执行上下文中存在全局变量a和函数A与函数B。

image.png

  1. 执行流进入A函数,在执行栈顶推入函数A的执行上下文,函数A的执行上下文存在局部变量a

image.png

  1. 执行函数A代码块中的函数B调用指令,执行栈顶推入函数B的执行上下文,函数B打印变量a

image.png

此时,控制台打印出了全局变量

讲到这里,我们不禁发出了疑问,根据执行栈的情况来说,理应是函数B逐层向下查找到函数A的执行上下文,并且打印出局部变量才对呀,为什么反而打印出了全局变量呢?

这里就引出了作用域这个概念

作用域

作用域:指的是一个变量的作用范围。
在ES6之前,JS的作用域只有全局作用域以及函数作用域两种,在ES6引入了块级作用域(本文暂且先不讨论块级作用域)。

一个变量如果是在全局环境下定义的,那么这个变量就存在于全局作用域下。以此类推,在函数内部定义的变量,则存在于函数作用域下。

image.png

在各自作用域下声明的变量只在各自作用域下有效。

作用域链

而JS的函数在声明时,会创建一个作用域链。它的用途是保证对执行上下文有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在的上下文的变量对象。如果这个上下文是函数,其变量对象为其内部声明的变量以及入参的arguments对象。作用域链中的下一个变量来自包含(外部)上下文,这样一直延续到全局执行上下文。而JS的函数在声明时,采用的是词法作用域,即在声明时就确定好了作用域链。作用域链的定义是静态的!

在上面的例子里,函数A和函数B在定义时,外部执行上下文只有全局执行上下文,所以其作用域链都为:

函数A/B作用域 -> 全局作用域

image.png

所以,上文中的例子,函数B内部通过其作用域链先找其内部的变量对象,发现没有变量a,便向上通过作用域链找到了全局作用域下的全局变量,并最终打印出结果。

个人理解:执行上下文所构成的执行栈,归并出了所有变量或函数。但是,并不是在栈顶的执行上下文就一定能向下获取所有的变量或函数,因为作用域的存在,规定了每个作用域内,只能通过作用域链去向下正确的访问各个变量或者函数。执行上下文是动态的,会根据调用函数环境的不同,存在不同的执行栈。而作用域链是静态的,是每个函数在创建之初根据词法作用域就生成的,它规定了在执行栈中,有权访问哪些变量或函数(纯个人总结,如有不对的地方,欢迎在评论区指出)

那么,怎么才能在全局作用域中获取函数作用域中的值呢?

闭包

说了这么多,闭包它终于来了。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的方式,就是在一个函数内部返回一个匿名函数。我们来改造一下上面的那个例子:

var a = "全局环境";

function A() {
  var a = "局部环境";
  return {
    B: function () {
      console.log(a);
    },
  };
}

var obj = A();
obj.B(); // "局部环境"

现在,我们在函数A的内部返回了一个对象,该对象内部有一个函数B。我们在全局环境下调用了这个函数B,结果打印出了局部环境。这就是一个闭包,我们成功的在全局作用域下调用到了函数A作用域中的变量。究竟是怎么回事呢?让我们再来执行一下这段代码:

  1. 全局执行上下文推入执行栈中,该上下文中存在一个全局变量a,一个函数A和一个对象obj。

image.png

  1. 调用函数A,执行栈中推入函数A执行上下文。该上下文中存在一个局部变量a。

image.png

  1. 执行obj.B(),将函数B执行上下文推入执行栈中。

image.png

  1. 打印变量a,此时根据词法作用域,我们画出作用域链。函数A是在全局环境下声明的,所以其作用域链的下一部分指向了全局作用域,而函数B是在函数A中声明的,所以其作用域链的下一部分指向了函数A的函数作用域。

image.png

此时,函数B的作用域链指向了函数A的作用域,因此打印出了局部变量。现在,我们通过闭包,可以在全局环境中使用函数作用域下的变量了,是不是感觉很棒。

抽象点来理解闭包:当一个函数内部返回了一个匿名函数,该匿名函数除了自身携带的物品外(内部的变量对象),还背着一个背包(通过作用域链引用的外部函数的变量对象)。在该函数外部就可以通过这个背包来访问这个函数内部的变量了。

注意点:因为闭包返回出来的匿名函数有对其外部函数变量对象的引用,原本外部函数执行完后其变量对象等都会被内存回收,但由于闭包的存在,其外部函数的变量对象还在被引用,所以不会内存回收。我们在使用闭包的同时,还要注意它所带来的副作用。

结语

闭包其实这个概念并不难以理解,只要了解了执行上下文作用域作用域链等概念,就能掌握闭包这个概念。而闭包在JS中的使用是非常广泛的,了解了闭包,相信你的JS功底会更加的扎实。由于本人也是在学习阶段,以上的文章如有不对的地方,欢迎在评论区里指出!

最后,码字不易,如果觉得我写的还不错的,烦请各位大佬点下宝贵的赞,你的支持是我创作的最大动力。QAQ