javascript深入闭包与作用域链底层原理(无惧面试)

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

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

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

GO{
string,
data,
number,
setimeout,
intrvieout,
window
}

我们在执行

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()

返回的函数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()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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()

你认为age会别销毁嘛?

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

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值