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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 图解 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 在一开始就是在堆中分配的。





目录
相关文章
|
2月前
|
存储 数据库 Android开发
🔥Android Jetpack全解析!拥抱Google官方库,让你的开发之旅更加顺畅无阻!🚀
【7月更文挑战第28天】在Android开发中追求高效稳定的路径?Android Jetpack作为Google官方库集合,是你的理想选择。它包含多个独立又协同工作的库,覆盖UI到安全性等多个领域,旨在减少样板代码,提高开发效率与应用质量。Jetpack核心组件如LiveData、ViewModel、Room等简化了数据绑定、状态保存及数据库操作。引入Jetpack只需在`build.gradle`中添加依赖。例如,使用Room进行数据库操作变得异常简单,从定义实体到实现CRUD操作,一切尽在掌握之中。拥抱Jetpack,提升开发效率,构建高质量应用!
51 4
|
3月前
|
Java 数据库连接
提升编程效率的利器: 解析Google Guava库之IO工具类(九)
提升编程效率的利器: 解析Google Guava库之IO工具类(九)
|
3月前
|
缓存 Java Maven
深入解析Google Guava库与Spring Retry重试框架
深入解析Google Guava库与Spring Retry重试框架
|
3月前
|
监控 安全 算法
提升编程效率的利器: 解析Google Guava库之RateLimiter优雅限流(十)
提升编程效率的利器: 解析Google Guava库之RateLimiter优雅限流(十)
|
3月前
|
缓存 安全 Java
提升编程效率的利器: 解析Google Guava库之集合工具类-50个示例(八)
提升编程效率的利器: 解析Google Guava库之集合工具类-50个示例(八)
|
3月前
|
缓存 算法 Java
提升编程效率的利器: 解析Google Guava库之常用工具类-40个示例(七)
提升编程效率的利器: 解析Google Guava库之常用工具类-40个示例(七)
|
3月前
|
存储
提升编程效率的利器: 解析Google Guava库之集合篇RangeMap范围映射(六)
提升编程效率的利器: 解析Google Guava库之集合篇RangeMap范围映射(六)
|
29天前
|
监控 网络协议 Java
Tomcat源码解析】整体架构组成及核心组件
Tomcat,原名Catalina,是一款优雅轻盈的Web服务器,自4.x版本起扩展了JSP、EL等功能,超越了单纯的Servlet容器范畴。Servlet是Sun公司为Java编程Web应用制定的规范,Tomcat作为Servlet容器,负责构建Request与Response对象,并执行业务逻辑。
Tomcat源码解析】整体架构组成及核心组件
|
1月前
|
存储 NoSQL Redis
redis 6源码解析之 object
redis 6源码解析之 object
55 6
|
13天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理

热门文章

最新文章

推荐镜像

更多