12 | 延迟解析:V8是如何实现闭包的?
该思维导图由 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-20315 - 熊杰有两个疑问。 希望解答一下。 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-2749 - Change老师,在堆中是如何存储这个内部变量的,又是如何区分其他内部变量的?不是很明白
作者回复: 不用区分啊,堆中所有的变量值都有引用,可以是栈中的引用,也可以是寄存器中的引用,还可以是堆中的引用,只要有引用,那么数据就是有用的!
2020-05-076 - saber应该都是分配在栈上,然后销毁foo的执行上下文的时候会有一个预解析的过程,检测到如果内部函数引用到了该作用域内变量,再将该变量放入到堆中存储。
作者回复: 是的
2020-04-1146 - lisiur很多概念还是很模糊 1. 预解析和真正的解析差别在哪(哪些事情是真正解析做的而预解析不做) 2. 预解析存在堆中的闭包数据和原始栈中数据是个什么关系 如何同步
作者回复: 比如预解释不生成ast,不生成作用域,只是快速查看内部函数是否引用了外部的变量,快速查看是否存在语法错误,这种执行速度非常快。 如果预解析的过程中,查看到了引用外部变量,那么V8就会将引用到的变量存放在堆中,并追加一个闭包引用,这样当上层函数执行结束之后,只要闭包突然引用了该变量,那么V8也不会销毁改变量。
2020-05-185 - HoSaltvar 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-282 - 李李这篇文章写的很好受益良多。 但老师有几个问题还是不太明白。 在JavaScript中闭包的定义是什么? 闭包会所带来什么隐性问题?(如:"内存泄露" 这种说法是怎么来的) 希望能得到老师的解答。
作者回复: 关于闭包的定义你剋参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures 函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。 另外内存泄漏其实就是不在需要的内存依然没有被释放。
2020-04-1422 - 灰的更高function outer(){ var a = 1; return function middle(){ return function inner(){ return a + 1; } } } 如果是这样的代码,在遇到middle函数的声明,预编译时,是否会检查inner函数中是否存在outer的局部变量?如果只是一层一层的执行和预编译,inner函数中的变量a还是获取不到。但是如果一次性预编译所有的代码,那么就会出现重复预编译的情况,老师能否解答下
作者回复: 会的,因为要保证是否释放outer函数中的a,如果存在任何引用,都不会释放
2020-05-131 - HoSalt老师,预解析、编译、执行三则的顺序是什么? 我理解预解析是要扫描全量代码,因为函数是可以嵌套很多层的,需要确认所有代码中是否引用了某个变量,包括eval中是否使用了
作者回复: 是的,eval是个特列,处理起来非常低效
2020-04-28 - Geek_177f82老师,这个预解析器和解析器是什么关系啊?
作者回复: 预解析器可以看成轻量级解析器,它的功能少,执行速度快
2020-04-13