图解 Google V8
李兵
前盛大创新院高级研究员
26763 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 25 讲
图解 Google V8
15
15
1.0x
00:00/00:00
登录|注册

12 | 延迟解析:V8是如何实现闭包的?

预解析器的作用
变量的销毁
函数作为返回值
内部函数访问父函数中定义的变量
在函数内部定义新的函数
实现方式
优点
执行阶段
编译过程
思考题
闭包带来的问题
闭包的特性
惰性解析
V8执行JavaScript代码
V8的闭包实现

该思维导图由 AI 生成,仅供参考

你好,我是李兵。
在第一节我们介绍过 V8 执行 JavaScript 代码,需要经过编译执行两个阶段,其中编译过程是指 V8 将 JavaScript 代码转换为字节码或者二进制机器代码的阶段,而执行阶段则是指解释器解释执行字节码,或者是 CPU 直接执行二进制机器代码的阶段。总的流程你可以参考下图:
代码执行
在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,这主要是基于以下两点:
首先,如果一次解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿。因为有时候一个页面的 JavaScript 代码都有 10 多兆,如果要将所有的代码一次性解析编译完成,那么会大大增加用户的等待时间;
其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直占用内存,特别是在手机普及的年代,内存是非常宝贵的资源。
基于以上的原因,所有主流的 JavaScript 虚拟机都实现了惰性解析所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

V8引入了惰性解析和预解析器来解决闭包带来的问题。惰性解析指解析器在解析过程中遇到函数声明时会跳过函数内部的代码,仅生成顶层代码的AST和字节码,从而加速JavaScript代码的启动速度。预解析器在解析顶层代码时,对函数进行快速预解析,检查语法错误和函数内部是否引用了外部变量,以解决闭包问题。由于闭包会引用当前函数作用域之外的变量,V8需要特殊处理,将引用的变量存放到堆中,即使当前函数执行结束后也不会释放该变量。这篇文章深入浅出地解释了V8实现闭包的复杂性,为读者提供了深入了解V8内部工作原理的视角。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《图解 Google V8》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(45)

  • 最新
  • 精选
  • 不二
    在编译阶段,v8不会对所有代码进行编译,要不然速度会很慢,严重影响用户体验,所以采用一种“惰性编译”或者“惰性解析”,也就是说 v8默认不会对函数内部的代码进行编译,只有当函数被执行前,才会进行编译。 而闭包的问题指的是:由于子函数使用到了父函数的变量,导致父函数在执行完成以后,它内部被子函数引用的变量无法及时在内存中被释放。 而闭包问题产生的根本原因是 javascript中本身的特性: 1. 可以在 JavaScript 函数内部定义新的函数; 2. 内部函数中访问父函数中定义的变量; 3. 因为 JavaScript 中的函数是一等公民,所以函数可以作为另外一个函数的返回值。 既然由于javascript本身的这种特性就会出现闭包的问题,那么我们就要想办法解决闭包问题,那么“预编译“ 或者“预解析” 就出现了, 预编译具体方案: 在编译阶段,v8不会完全不解析函数,而是预解析函数,简单理解来说,就是判断一下父函数中是否有被子函数饮用的变量,如果有的话,就需要把这个变量copy一份到 堆内存中,同时子函数本身也是一个对象,它会被存在堆内存中,这样即使父函数执行完成,内存被释放以后,子函数在执行的时候,依然可以从堆内存中访问copy过来的变量。

    作者回复: 对

    2020-05-20
    3
    15
  • 熊杰
    有两个疑问。 希望解答一下。 1. 如果有闭包,函数是执行完毕再进行堆复制的吧? 2. 堆复制后。 变量地址是怎么跟真正有引用关系的未编译的函数保持关系的。 这个引用是否直接存放在未编译的函数对象上?

    作者回复: 我们可以看下面一段简单的闭包代码: function main() { let a = 1 let b = 2 let c = 3 return function foo() { return c } } let inner = main() 使用d8来打印这段代码的作用域: Global scope: function main () { // (0x7fca29051668) (13, 112) // will be compiled // 2 stack slots // 3 heap slots // local vars: LET b; // (0x7fca290519d0) local[1], never assigned, hole initialization elided LET c; // (0x7fca29051ab8) context[2], forced context allocation, never assigned LET a; // (0x7fca290518e8) local[0], never assigned, hole initialization elided function foo () { // (0x7fca29051b70) (83, 110) // lazily parsed // 2 heap slots } } 可以看出,let c后面是这样描述的 LET c; // (0x7fca29051ab8) context[2], forced context allocation, never assigned 说明c在一开始就是在堆中分配的。 堆复制的这样情况也是存在的,那就是使用eval,这种方式没办法提前解析,所以eval是非常影响效率的一种方式

    2020-05-27
    4
    9
  • Change
    老师,在堆中是如何存储这个内部变量的,又是如何区分其他内部变量的?不是很明白

    作者回复: 不用区分啊,堆中所有的变量值都有引用,可以是栈中的引用,也可以是寄存器中的引用,还可以是堆中的引用,只要有引用,那么数据就是有用的!

    2020-05-07
    6
  • saber
    应该都是分配在栈上,然后销毁foo的执行上下文的时候会有一个预解析的过程,检测到如果内部函数引用到了该作用域内变量,再将该变量放入到堆中存储。

    作者回复: 是的

    2020-04-11
    4
    6
  • lisiur
    很多概念还是很模糊 1. 预解析和真正的解析差别在哪(哪些事情是真正解析做的而预解析不做) 2. 预解析存在堆中的闭包数据和原始栈中数据是个什么关系 如何同步

    作者回复: 比如预解释不生成ast,不生成作用域,只是快速查看内部函数是否引用了外部的变量,快速查看是否存在语法错误,这种执行速度非常快。 如果预解析的过程中,查看到了引用外部变量,那么V8就会将引用到的变量存放在堆中,并追加一个闭包引用,这样当上层函数执行结束之后,只要闭包突然引用了该变量,那么V8也不会销毁改变量。

    2020-05-18
    5
  • HoSalt
    var strFn = 'function xx(){console.log(y)}; xx();' function a() { var x = 1 var y ='我是y' return function b () { return function c(){ eval(strFn) } } } 老师,如果是这种eval动态执行的怎么预解析,又是怎么处理的作用域的问题的?

    作者回复: eval会造成将栈中的数据复制到堆中的情况,这种情况效率低下

    2020-04-28
    2
  • 李李
    这篇文章写的很好受益良多。 但老师有几个问题还是不太明白。 在JavaScript中闭包的定义是什么? 闭包会所带来什么隐性问题?(如:"内存泄露" 这种说法是怎么来的) 希望能得到老师的解答。

    作者回复: 关于闭包的定义你剋参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures 函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。 另外内存泄漏其实就是不在需要的内存依然没有被释放。

    2020-04-14
    2
    2
  • 灰的更高
    function outer(){ var a = 1; return function middle(){ return function inner(){ return a + 1; } } } 如果是这样的代码,在遇到middle函数的声明,预编译时,是否会检查inner函数中是否存在outer的局部变量?如果只是一层一层的执行和预编译,inner函数中的变量a还是获取不到。但是如果一次性预编译所有的代码,那么就会出现重复预编译的情况,老师能否解答下

    作者回复: 会的,因为要保证是否释放outer函数中的a,如果存在任何引用,都不会释放

    2020-05-13
    1
  • HoSalt
    老师,预解析、编译、执行三则的顺序是什么? 我理解预解析是要扫描全量代码,因为函数是可以嵌套很多层的,需要确认所有代码中是否引用了某个变量,包括eval中是否使用了

    作者回复: 是的,eval是个特列,处理起来非常低效

    2020-04-28
  • Geek_177f82
    老师,这个预解析器和解析器是什么关系啊?

    作者回复: 预解析器可以看成轻量级解析器,它的功能少,执行速度快

    2020-04-13
收起评论
显示
设置
留言
45
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部