likes
comments
collection
share

从JS调用栈/作用域/作用域链/环境对象到闭包原理的深入理解

作者站长头像
站长
· 阅读数 21

什么是闭包

概念

一句话概括:函数外访问函数内部变量

如果想要简单的理解闭包,可以使用上面这句话。但超出了JS的执行原理,所以说它不够严谨,因为这里所指代的访问是间接访问。

实现前提:

  • 函数嵌套
  • 内层函数在外调用
// 1. inner函数在outer函数内层
// 2. inner通过作用域链原理访问outer函数环境变量a
// 3. inner函数通过outer函数return
// 4. outer外部访问inner

function outer() {
    var a = 0;
    
    // 1
    function inner() {
        // 2
        console.log(a++);
    }
    
    // 3
    return inner;
}

// 4
var func = outer();
func();  // log: 0
func();  // log: 1

闭包的特性

函数执行完成后内部执行环境对象以及环境变量所占用内存不会被释放

  • 执行环境对象被引用(JS垃圾回收机制:标记清除法,环境对象一直会被标记上,所以不会被回收,除非4标记处的func函数取消引用,设为null)

闭包产生的核心概念以及它们的关系

想要理解闭包原理,至少需要深入理解JS 执行环境作用域/作用域链/环境对象

作用域/作用域链/环境对象

作用域:当前的执行上下文,值和表达式在其中“可见”或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。

作用域链:JS和其他编程语言一样,访问一个变量,优先从当前作用域查找,若没有查到对应变量,则依次访问父作用域,直到查找到此变量或者直到全局作用域也未被找到。这样依次访问的过程其实就是一个作用域链的形成过程。

从JS调用栈/作用域/作用域链/环境对象到闭包原理的深入理解 环境对象(准确说是“代码执行的作用域环境对象”):执行环境唯一,代码执行实时环境,每一个环境对象对应一个作用域,每当代码执行过程新建一个新的作用域(如执行函数,创建函数作用域),会创建一个环境对象并作为当前环境对象,这个环境对象包含当前以及上层环境变量的所有变量的引用(这就是函数内能访问外层变量的原因)。上述解释可能不易理解,我们看下图: 从JS调用栈/作用域/作用域链/环境对象到闭包原理的深入理解 图中所有的环境变量也会形成链表,且与作用域链一一对应,链表的表头是最外层的全局变量环境,表尾是当前代码执行位置

执行栈、作用域链和环境对象

简单理解:执行栈压栈和作用域链的添加以及环境对象的生成是是一一对应的,也就是说压入一个执行函数就产生一个作用域并生成一个环境对象。

为更好理解,我们按照代码执行前后关系并在几个重要执行节点打上断点,我们看下图:

从JS调用栈/作用域/作用域链/环境对象到闭包原理的深入理解

图中清晰展现出内层作用域与外层作用域的关系,外层作用域对应的环境对象对内层作用域是可见的。

可是,这和闭包有什么关系?让我们开始本文核心(看到这里,我期望你已经完全理解了 作用域、作用域链、环境变量 的概念以及它们之间的关系)。

作用域回收与环境对象引用转移(闭包产生核心)

首先要告诉大家,上面四步Debug图中的Debug3和Debug4对应的作用域链部分是错误的,正确的是:

从JS调用栈/作用域/作用域链/环境对象到闭包原理的深入理解

可以看到作用域链中"p outer"作用域已经丢失了对outer环境对象的引用,会被回收;但因"p inner"作用域引用了outer环境对象中的a变量,所以在回收"p outer"之前,会将outer环境对象的引用会转移到"p inner"的"Closure"上,用于保持上下层作用域之间的环境对象可见性。

后续

如果理解了上一段落的解释,就很好理解:

  1. 内存泄漏的产生
  2. 用于保护变量、延长变量生命周期、实现模块化

参考

developer.mozilla.org/zh-CN/docs/…

转载自:https://juejin.cn/post/7393740927154815012
评论
请登录