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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析DNS,个人版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 图解 Google V8 # 12:延迟解析:V8是如何实现闭包的?

说明

图解 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 函数的内部代码生成抽象语法树。


f54ffa6d8e244bf09e15611c8fd1f5f7.png


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

2c9dcb84c2114b16be22b88235682138.png


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


  1. 首先会先执行 a=1c=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, b) {
        const c = a + b + d
        return c
    }
}
const f = foo()


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


   当调用 foo 函数时,foo 函数会将它的内部函数 inner 返回给全局变量 f;


   然后 foo 函数执行结束,执行上下文被 V8 销毁;


   虽然 foo 函数的执行上下文被销毁了,但是依然存活的 inner 函数引用了 foo 函数作用域中的变量 d。


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


6026aa94e6584cfa825816225cc57526.png



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 里使用新的代码


249848c46c144f289225ebfd4eee1c5b.png


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


078fb24696b24d34811ea0029ca0082f.png


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

v8-debug --print-scopes kaimo.js



74174760b50e4dc3ba24eeacdabd057c.png



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





目录
相关文章
|
16天前
|
Java 数据库连接
提升编程效率的利器: 解析Google Guava库之IO工具类(九)
提升编程效率的利器: 解析Google Guava库之IO工具类(九)
|
16天前
|
缓存 Java Maven
深入解析Google Guava库与Spring Retry重试框架
深入解析Google Guava库与Spring Retry重试框架
|
16天前
|
监控 安全 算法
提升编程效率的利器: 解析Google Guava库之RateLimiter优雅限流(十)
提升编程效率的利器: 解析Google Guava库之RateLimiter优雅限流(十)
|
16天前
|
缓存 安全 Java
提升编程效率的利器: 解析Google Guava库之集合工具类-50个示例(八)
提升编程效率的利器: 解析Google Guava库之集合工具类-50个示例(八)
|
16天前
|
缓存 算法 Java
提升编程效率的利器: 解析Google Guava库之常用工具类-40个示例(七)
提升编程效率的利器: 解析Google Guava库之常用工具类-40个示例(七)
|
16天前
|
存储
提升编程效率的利器: 解析Google Guava库之集合篇RangeMap范围映射(六)
提升编程效率的利器: 解析Google Guava库之集合篇RangeMap范围映射(六)
提升编程效率的利器: 解析Google Guava库之集合篇RangeSet范围集合(五)
提升编程效率的利器: 解析Google Guava库之集合篇RangeSet范围集合(五)
|
2月前
|
数据可视化 定位技术 Sentinel
如何用Google Earth Engine快速、大量下载遥感影像数据?
【2月更文挑战第9天】本文介绍在谷歌地球引擎(Google Earth Engine,GEE)中,批量下载指定时间范围、空间范围的遥感影像数据(包括Landsat、Sentinel等)的方法~
1349 1
如何用Google Earth Engine快速、大量下载遥感影像数据?
|
2月前
|
编解码 人工智能 算法
Google Earth Engine——促进森林温室气体报告的全球时间序列数据集
Google Earth Engine——促进森林温室气体报告的全球时间序列数据集
51 0
|
2月前
|
编解码 人工智能 数据库
Google Earth Engine(GEE)——全球道路盘查项目全球道路数据库
Google Earth Engine(GEE)——全球道路盘查项目全球道路数据库
68 0

推荐镜像

更多