• 一文带你深入闭包与作用域链原理(无惧面试)


    前言:如果有不理解的地方可以评论或私信,我会每一条都回复。建议大家看一下上一章V8引擎执行原理,里面将解释一些原理与本文相联系。(这将是一个系列)

    一,作用域链,作用域上下文,内存管理

    1,在V8引擎执行过程中,js到ast树中间,js在被解析的时候创建了一个对象,叫全局对象GO

    GO{
    string,
    data,
    number,
    setimeout,
    intrvieout,
    window
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们在执行

    var name = ‘hello’

    var age = 18

    这时候会将其编译,添加到GO里面。这时候代码还未执行,执行在字节码到-cpu,cpu来执行代码那个过程。这里编译后og里会出现name:undifand,num:undifand,所以这就是我们在第一行代码前加上log(‘name’)的时候会打印undifand的原因。只因为在执行log的时候var name = ‘zlk’ 还未执行,但是已经被编译,可以找到。它在GO里,控制器打印window会找到。

    补充理解:v8里面有执行上下文栈,栈里有VO对应着GO,执行代码的时候会在VO里找相应的变量,这里可以理解为在GO里找因为VO对应GO。

    在这里插入图片描述

    2,但是我们执行函数的时候就不一样了,同样js在编译阶段,将函数名编译到GO里面但是这时候电脑内存会开辟空间,将函数体和父作用域放进去去,而GO里的函数名后是这个内存的地址。(这就是为什么函数调用写前后都会被执行的原因,因为og里后面是内存地址)

    之后v8在执行上下文栈中创建个函数执行上下文,里面有vo对应ao,ao里面是我们函数体里声明的变量函数等键值对,同样会进行编译,但是函数没有执行阶段,变量值为undifand,等执行过后赋值,之后函数执行上下文会被销毁,如果再次调用会再次执行相同操作。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aICldOb3-1669547184796)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221126161647814.png)]

    函数执行完后 函数执行上下文FEC会被销毁 ,如果FEC被销毁了 就没有AO就没有被VO指向,所以AO也会被销毁。如果我们在代码中之后又调用了函数,此时会重新创建函数执行上下文,指向重新创建的AO

    3,作用域链,v8引擎是安作用域链去查找的,先看AO里面有没有,没有去上级作用域去查找。这里函数foo的上级作用域是全局对象GO,在执行log(name) 的时候,GO里name这时候已经被赋值hello

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5TxE0RE-1669547184798)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221126160124179.png)]

    作用域提升解析

    请添加图片描述

    从左数第一个

    我们来判断一下log打印的值是多少,思考一下看解析

    解析:

    首先解析代码创建全局GO对象,里面有n:undifaned foo:开辟foo存储空间的内存地址

    执行代码第一行 ,GO里n被赋值为100

    执行代码到第四行的时候,执行函数foo过程为,在全局执行上下文栈中开辟函数执行上下文。

    创建AO 被VO指向?no

    原本要创建AO 的,但是我们函数体中没有var n = 值 var b = 值 或者函数,对象。所以我们这里没有创建AO, 执行函数体中n = 200 它先在自己的AO里找,发现没有n,再去上级作用域GO里面找,GO里面有n 并给n赋值,这时候n被改为200。执行到最后一行打印n 去当前作用域GO 里面找这时n = 200

    答案:200

    第二个

    解析:同样创建GO foo:内存地址,n:undifaned 开辟函数存储空间 开辟函数上下文,创建ao n:undifaned

    执行代码到倒数第二行GO中 n 被赋值 100 。

    执行代码到最后一行 去执行函数体

    执行函数体中第一行 打印n 为undifaned (AO已经创建 里面n为undifaned,不用去上层作用域查找。我们是有值的只不过值为undifaned)

    函数体第二行 ao 中 n 被赋值为200

    函数体第三行 打印n为200

    答案:

    第三个

    解析:foo1中log回去本作用域找,没有后去上层作用域GO找为100, foo2中log会去本作用域AO找,为200,全局Log会在本作用域GO找为100

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iL479AzO-1669547184803)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221126183846367.png)]

    4,内存管理:js内存管理分堆和栈,堆是放复杂数据类型,栈放基本数据类型,堆中复杂数据类型会将指针返回值,返回到栈中变量引用。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNHgj9Qy-1669547184806)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221126185100521.png)]

    5,js自动管理内存,创建与回收,回收机制是看被指向次数,如果为0,销毁。比如在堆中,我们有个复杂数据类型体obj2指向了另一个复杂类型体obj,obj3指向obj 。栈中声明的obj的值地址也会指向存在堆中的obj,这时堆中的obj现在数值为3,不会被销毁,如果没有其他数据指向它,它会被销毁。比如obj2 = null obj 3 = null 为retaincount:1 再将obj = null 这时retaincount:0 被回收

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RO54B42x-1669547184808)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221126185824439.png)]

    6,循环内存泄漏,如果有两个复杂类型相互指向,就永远不会被销毁,这就是循环内存泄露。

    二:闭包的认识

    概念:闭包是个函数,它可以访问外部定义的自由变量

    广义的角度说,js里的函数都是闭包,但这不严谨。

    闭包案例

        function foo() {
          var name = 'zlk'
          function bar() {
            console.log(name);
          }
          return bar
        }
    
        var fn = foo()
        fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    返回的函数bar可以访问外部自由变量name这就是典型闭包,让我们看一下它内部执行流程。

    流程图一(注意:两张图不一样,foo被销毁了,创建了bar函数执行上下文)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oaThQFvA-1669547184810)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221127173933780.png)]

    流程图二

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rb8TbgN1-1669547184812)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221127173955654.png)]

    首先创建全局执行上下文

    创建GO对象编译 全局执行上下文中VO:GO

    开辟foo函数存储空间

    创建函数执行上下文

    创建foo函数的AO对象编译

    VO指向AO VO:AO

    执行函数体 给AO对象中变量赋值

    返回值为bar地址 所以全局执行上下文中fn为bar的地址0x00b

    那么它对应的GO里的fn变量从undifaned被赋值为bar地址0x00b

    函数执行完毕foo被销毁

    创建bar函数执行上下文

    创建bar函数的AO对象为空,里面只是一个log打印

    bar函数执行,本AO里没有name变量,去上级作用域里面找,foo的AO里有name,所以被打印zlk(你会疑惑foo函数不是被销毁了嘛,为什么它的AO还在?这就是闭包的内存泄露,下面有解释)

    bar函数执行完毕被销毁

    三:闭包的内存泄露

    function foo() {
      var name = 'zlk'
      var age = 18
      function bar() {
        console.log(name);
      }
      return bar
    }
    
    var fn = foo()
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csRu4UwB-1669547184814)(C:\Users\jia\AppData\Roaming\Typora\typora-user-images\image-20221127181921946.png)]

    还是上面的典型闭包,我们来分析一下它的内存泄露,仔细看箭头指向

    我们一开始就说过 开闭的函数空间中有两部分,一个是上级作用域,另一个是此函数体,由于画图比较麻烦中间没有细画

    我们知道GO编译后会开辟内存空间 GO中变量foo指向函数内存空间0x00a地址。我们想一下foo函数存储空间上级作用域是什么?是全局对象GO。GO也是有内存地址的,所以他们互相指向,同理foo的AO和bar函数存储空间也互相指向。

    当foo 返回一个bar的内存地址后,我们说过 全局上下文中fn 为 bar内存地址0x00b ,那么它对应的GO中 fn会被赋值为0x00b。

    我们说过内存管理中,如果没有指向了,就会被销毁。当我们函数foo执行完后会被销毁。

    在这里插入图片描述

    为什么它对应的AO没有被销毁呢?因为GO中fn指向着bar的函数空间,所以bar函数空间不会被销毁,bar和foo的AO也互相指向。所以foo的AO也不会被销毁。这就是为什么闭包函数bar访问作用域链上级foo的AO中name变量会成功访问。

    第四:闭包的自由变量泄露

    function foo() {
      var name = 'zlk'
      var age = 18
      function bar() {
        console.log(name);
      }
      return bar
    }
    
    var fn = foo()
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    你认为age会别销毁嘛?

    我们知道foo 函数创建函数执行上下文后 VO指向AO AO编译赋值后 会有变量name:‘zlk’ 和 age:18

    那么我们知道foo执行完会被销毁,但是它的AO被别人指向,无法销毁。

    但是age在代码中没有被用到,我们的v8引擎会算法识别出来age,age会被销毁。

    所以AO不会被销毁,但自由变量age没被用到会被销毁。

  • 相关阅读:
    web 基础和http 协议
    java校园快递代领系统 小程序
    RunwayGen2上线全新控制功能「运动笔刷」
    基于 BIO 形式下的文件上传
    跨境电商影响搜索排名的因素有哪些
    go环境部署
    windows c++开发
    常见插件 tomcat插件
    项目文章| PBJ(IF:13.8)发表稻曲病菌效应因子Uv1809增强组蛋白去乙酰化抑制水稻免疫的分子机制
    centos7.9编译安装libzip-1.9.2 和 cmake 3.23.0
  • 原文地址:https://blog.csdn.net/weixin_44600810/article/details/128180767