说明
图解 Google V8 学习笔记
什么是惰性解析?
所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。
在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,如果一次性解析和编译所有 JavaScript 代码会导致下面的问题:
会增加编译时间,影响到首次执行 JavaScript 代码的速度。
解析完成的字节码和编译之后的机器代码将会一直占用内存。
基于以上的原因,所有主流的 JavaScript 虚拟机都实现了惰性解析。
惰性解析的过程
结合下面的例子分析:
function foo(a,b) { var d = 100 var f = 10 return d + f + a + b; } var a = 1 var c = 4 foo(1, 5)
V8 会至上而下解析这段代码,先遇到 foo 函数,会将函数声明转换为函数对象,但是并没有解析和编译函数内部的代码,不会为 foo 函数的内部代码生成抽象语法树。
然后继续往下解析,后续的代码都是顶层代码,所以 V8 会为它们生成抽象语法树:
代码解析完成之后,V8 便会按照顺序自上而下执行代码
- 首先会先执行
a=1
和c=4
这两个赋值表达式 - 接下来执行 foo 函数的调用,过程是从 foo 函数对象中取出函数代码,V8 会先编译 foo 函数的代码,编译时同样需要先将其编译为抽象语法树和字节码,然后再解释执行。
JavaScript 的三个特性
- JavaScript 语言允许在函数内部定义新的函数
- 可以在内部函数中访问父函数中定义的变量
- 因为函数是一等公民,所以函数可以作为返回值
闭包给惰性解析带来的问题
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
使用 JavaScript 三个特性组装一段经典的闭包代码:
function foo() { var d = 20 return function inner(a, b) { const c = a + b + d return c } } const f = foo()
上面这段代码的执行过程:
当调用 foo 函数时,foo 函数会将它的内部函数 inner 返回给全局变量 f;
然后 foo 函数执行结束,执行上下文被 V8 销毁;
虽然 foo 函数的执行上下文被销毁了,但是依然存活的 inner 函数引用了 foo 函数作用域中的变量 d。
当执行 foo 函数的时候,堆栈的变化:
foo 函数的执行上下文虽然被销毁了,但是 inner 函数引用的 foo 函数中的变量却不能被销毁,那么 V8 就需要为这种情况做特殊处理,需要保证即便 foo 函数执行结束,但是 foo 函数中的 d 变量依然保持在内存中,不能随着 foo 函数的执行上下文被销毁掉。
那么怎么处理呢?
在执行 foo 函数的阶段,虽然采取了惰性解析,不会解析和执行 foo 函数中的 inner 函数,但是 V8 还是需要判断 inner 函数是否引用了 foo 函数中的变量,负责处理这个任务的模块叫做预解析器。
预解析器如何解决闭包所带来的问题?
V8 引入预解析器,当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析,目的:
判断当前函数是不是存在一些语法上的错误
检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题。
预解释不生成 ast,不生成作用域,只是快速查看内部函数是否引用了外部的变量,快速查看是否存在语法错误,这种执行速度非常快。
如果预解析的过程中,查看到了引用外部变量,那么V8就会将引用到的变量存放在堆中,并追加一个闭包引用,这样当上层函数执行结束之后,只要闭包突然引用了该变量,那么V8也不会销毁改变量。
注意:eval 没办法提前解析,会造成将栈中的数据复制到堆中的情况,这种情况效率低下。
拓展例子:需要安装 jsvu ,具体看window 系统里怎么使用 jsvu 工具快速调试 v8?
我们在 kaimo.js 里使用新的代码
function main() { let a = 1 let b = 2 let c = 3 return function inner() { return c } } let kaimo = main()
然后执行下面的命令查看作用域
v8-debug --print-scopes kaimo.js
我们可以看到 let c后面是这样描述的: LET c; // (0000015AFAC2F5E0) context[2], forced context allocation, never assigned
,说明 c 在一开始就是在堆中分配的。