JS - 理解闭包(最简单的闭包理解,最直观的感受,最不容易忘的理解方式,一次就会)

138 阅读3分钟

闭包的理解

一个函数访问了外部的自由变量,就会形成一个闭包。

作用域链

在函数执行时,函数内部的变量访问修改等都需要明确这个变量是谁,这些变量其实都存放在函数的作用域链中。JS的函数作用域链实在函数声明和定义时生成的,所以也叫静态作用域链(词法作用域链),与之对应的是动态作用域链(函数执行的时候生成的作用域链,譬如LISP语言,另外js中函数执行的时候会在静态作用域链中插入一个作用域数据对象,也可以理解为是动态作用域链)

闭包和作用域链有啥关系

函数的作用域链是个数组,其中数组中存放的就是各个作用域下该函数要访问的变量,其中如果是外部函数,则是一个闭包作用域。

写个例子来看看

  function A() {
      var a1 = 'a1'
      var a2 = {a: '1'}
      
      function B() {
          console.log('A:a1,a2:', a1, a2)
          var b1 = 'b1'
          var b2 = {b: '2'}
          
          function C() {
             console.log('B:b1,b2:', b1, b2)
             var c1 = 'c1'
             var c2 = {c: '3'}
          }
          return C
      }
      return B
  }
  var bFn = A()
  var cFn = bFn()
  cFn()
  

接下来我们看看上面这段代码在函数B,函数C定义以及执行的时候,分别长啥样,来看一看这个作用域链下到底存了什么值

image.png

我们先看把B函数加到watch里,断点也分别打在函数B声明定义以及B函数执行时。我们观察一下B在定义之后watch中B长啥样,以及B执行时Scope长啥样。

image.png

我们可以发现当B定义完之后,可以看到B这个函数(函数也是对象)的所有属性,其中B函数有个属性叫Scopes,被[[]]所引用了,这个属性只能在浏览器调试模式下看到,在运行时调用是访问不到这个属性的,这个属性时语言层面的属性。我们接下来着重关心一下这个Scopes里存了什么数据。我们可以看到这个Scopes是个数组结构,其中有2个元素,第一个元素是个对象,这个对象的名字叫Closure(A)。其实就是一个闭包作用域对象,这个对象里有2个key分别是a1,a2,其实就是函数B访问的函数A中的2个变量。Scopes的下标1的元素是Global也就是window对象。这里我们猜想一下函数B定义时这个Scopes是以什么规律生成的呢。函数B实在函数A执行的时候定义的,那么函数B的作用域链就是函数A的作用域链在插入一个函数B要访问的作用域(如果函数B不访问A中的变量,那么其实B函数的作用域链和函数A的作用域链是长的一样的)。

image.png

我们看到函数B执行时,Scope其实也是个数组,这个数组比B函数里的Scopes多了一个Local。我们可以吧这个函数B执行时的Scope理解为动态作用域链。这个Local是函数执行时动态生成的,其中我们看到了this(这个只有在函数执行时才能确认的家伙),还有函数B内部的几个变量分别是b1,b2,C。那么有了这个动态作用域链,函数B在执行的时候访问变量是不是就贼鸡儿方便了。直接沿着这个链去找就行了,找不到就undefined。

函数C定义和执行我们就不赘述了,直接上图:

image.png

image.png