0
点赞
收藏
分享

微信扫一扫

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

司马吹风 2022-08-18 阅读 109


说明

图解 Google V8 学习笔记

什么是惰性解析?

所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。

在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,如果一次性解析和编译所有 JavaScript 代码会导致下面的问题:

  1. 会增加编译时间,影响到首次执行 JavaScript 代码的速度。
  2. 解析完成的字节码和编译之后的机器代码将会一直占用内存。

基于以上的原因,所有主流的 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 函数的内部代码生成抽象语法树。

图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?_v8

然后继续往下解析,后续的代码都是顶层代码,所以 V8 会为它们生成抽象语法树:

图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?_javascript_02

代码解析完成之后,V8 便会按照顺序自上而下执行代码

  1. 首先会先执行​​a=1​​​ 和​​c=4​​ 这两个赋值表达式
  2. 接下来执行 foo 函数的调用,过程是从 foo 函数对象中取出函数代码,V8 会先编译 foo 函数的代码,编译时同样需要先将其编译为抽象语法树和字节码,然后再解释执行。

JavaScript 的三个特性

  1. JavaScript 语言允许在函数内部定义新的函数
  2. 可以在内部函数中访问父函数中定义的变量
  3. 因为函数是一等公民,所以函数可以作为返回值

闭包给惰性解析带来的问题

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

使用 JavaScript 三个特性组装一段经典的闭包代码:

function foo() {
var d = 20
return function inner(a,) {
const c = a + b + d
return c
}
}
const f = foo()

上面这段代码的执行过程:

  1. 当调用 foo 函数时,foo 函数会将它的内部函数 inner 返回给全局变量 f;
  2. 然后 foo 函数执行结束,执行上下文被 V8 销毁;
  3. 虽然 foo 函数的执行上下文被销毁了,但是依然存活的 inner 函数引用了 foo 函数作用域中的变量 d。

当执行 foo 函数的时候,堆栈的变化:

图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?_javascript_03

foo 函数的执行上下文虽然被销毁了,但是 inner 函数引用的 foo 函数中的变量却不能被销毁,那么 V8 就需要为这种情况做特殊处理,需要保证即便 foo 函数执行结束,但是 foo 函数中的 d 变量依然保持在内存中,不能随着 foo 函数的执行上下文被销毁掉。

那么怎么处理呢?

在执行 foo 函数的阶段,虽然采取了惰性解析,不会解析和执行 foo 函数中的 inner 函数,但是 V8 还是需要判断 inner 函数是否引用了 foo 函数中的变量,负责处理这个任务的模块叫做预解析器

预解析器如何解决闭包所带来的问题?

V8 引入预解析器,当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析,目的:

  1. 判断当前函数是不是存在一些语法上的错误
  2. 检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题。

预解释不生成 ast,不生成作用域,只是快速查看内部函数是否引用了外部的变量,快速查看是否存在语法错误,这种执行速度非常快。

如果预解析的过程中,查看到了引用外部变量,那么V8就会将引用到的变量存放在堆中,并追加一个闭包引用,这样当上层函数执行结束之后,只要闭包突然引用了该变量,那么V8也不会销毁改变量。

注意:eval 没办法提前解析,会造成将栈中的数据复制到堆中的情况,这种情况效率低下

拓展例子:需要安装 jsvu ,具体看​​window 系统里怎么使用 jsvu 工具快速调试 v8?​​

我们在 ​​kaimo.js​​ 里使用新的代码

图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?_javascript_04

function main() {
let a = 1
let b = 2
let c = 3
return function inner() {
return c
}
}

let kaimo = main()

图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?_javascript_05

然后执行下面的命令查看作用域

v8-debug --print-scopes kaimo.js

图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?_v8_06

我们可以看到 let c后面是这样描述的:​​ LET c; // (0000015AFAC2F5E0) context[2], forced context allocation, never assigned​​,说明 c 在一开始就是在堆中分配的。

参考资料

  • ​​MDN:闭包​​
  • ​​window 系统里怎么使用 jsvu 工具快速调试 v8?​​


举报

相关推荐

0 条评论