从原理谈闭包简单易懂!

1,116 阅读5分钟

其实闭包很简单,你只不过是被网上大量的文章吓到了,这真的是一个很简单的知识点。认真看我的讲解。

首先,我先说一下总结,其实闭包只是作用域链的副产物罢了,想要彻底的搞懂闭包,必须先拿作用域链开刀!!

接下来就是作用域链的讲述。

一 作用域链

不过在想要搞定作用域链之前,我们要先知道词法环境。

在 js 中,每个运行的函数,代码块或整个程序,都有一个称为词法环境的关联对象。

词法环境由两个部分组成:

所以,变量只是环境记录这个特殊内部对象的属性。访问或修改变量意味着访问或改变词法环境的一个属性。

举个全局词法环境的例子: 对于上图来说,这是一个与整个脚本相关联的全局词法环境,矩形表示环境记录(存放变量),箭头表示外部词法环境的引用。由于全局词法环境没有外部引用,所以在这里指向 null。

我们继续对上图进行一个更细致到解释:

  • 当脚本开始运行,词法环境预先填充了所有声明的变量。最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是不允许在 let 之前使用它。几乎就像变量不存在一样。
  • 然后直到“let name”语句的出现,因为 name 没有被初始化,所以它的值是 undefined,但是我们是可以使用这个变量的。
  • 再到“name = 小明”的执行,这个 name 变量被赋予了一个值。

以上是对变量的词法环境的解释,接下来我们看函数声明的。

对于函数来说,一个函数其实也是一个值,就像变量一样,和变量不同的是,函数声明在初始化时是被立即完成的。

当创建一个新的词法环境时,经过函数声明的函数会立马可以被使用。(不会像变量一样直到声明时才可以使用)

正常来说,这种情况只适合于函数声明,不适合于函数表达式。

下面我们再看看更为复杂的词法环境。

在say函数被调用时,会立即创建一个新的词法环境用于存储这个函数中的变量和参数。

在say函数被执行时,就出现了两个词法环境,一个时say函数的词法环境,另一个就是全局(js文件)的词法环境。

  • say 函数的词法环境当中有一个属性,当我们调用 say(“中国加油!”)时,所以 words 的值就是“中国加油!”。
  • say 函数的外部词法环境引用指向了全局词法环境,它具有 say 和 name。

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。这就是作用域链。一级一级的往上找,直到找到,找不到就报错。

我们来演示一下查找的过程:

  • 对于 words 变量来说,会立即在本词法环境中找到 words 的值。
  • 对于 name 变量来说,会先在 say 函数词法环境中找,找不到才会去全局词法环境中找。

作用域链:当查找变量的时候,会先从当前环境记录对象中查找,如果没有找到,就会从父级(词法层面上的父级)环境记录对象中查找,一直找到全局环境记录对象,也就是全局对象。这样由多个环境记录对象构成的链表就叫做作用域链。

在搞懂作用域链之后,就要开始进入我们今天的重头戏,闭包!!

二 闭包

接下来我就用一个计数器的例子来为大家讲解闭包!

function makeCounter () {
    let counter = 0;
    return function () {
        return counter++;
    }
}
let counter = makeCounter();

在makeCounter() 函数被调用的时候,会创建一个新的词法环境,用于存储mackCounter运行时的变量。如下图:

在执行makeCounter()函数时,我们创建了一个嵌套函数:return counter++,但是它没有被执行,仅仅只是被创建了。

每个函数被创建的时候都会记住创建它们的词法环境,这些词法环境的引用被永久保留在函数的隐藏属性[[Environment]]中。如下图所示:

因此,counter.[[Environment]]中有对{counter: 0}的词法环境引用,这个就是函数会记住它在何处被创建的原因,和在哪里调用函数无关,[[Environment]]引用在函数被创建的时候赋值并永久保留。

当在调用counter()时,会创建一个新的词法环境,该外部词法环境引用的值获取于该函数[Environment]]中的值。如下图:

当counter()在自己的词法环境中找counter变量时,没有找到(环境记录对象为空),会到它的父级(makeCounter词法环境)中寻找,并且找到了counter变量,并且在这个环境记录对象中修改这个变量的值。

在变量所处的词法环境中更新变量。

这个就是闭包产生的原因,所以说闭包只是作用域链的副产物。

闭包 是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。在某些编程语言中,这是不可能的,或者应该以特殊的方式编写函数来实现。但是如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。也就是说:JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量。