延迟解析:V8是如何实现闭包的?
在编译JavaScript代码的过程中,V8并不会一次性将所有的JavaScript解析为中间代码,这主要是基于以下两点:
首先,如果是一次性解析和编译所有代码,会影响首次编译的时间,会大大增加用户等待时间。
其次,解析完成的字节码和编译之后的机器代码会存在内存中,一次性解析所有JavaScript会一直占用内存,影响性能。
基于以上原因,所有主流的JavaScript虚拟机都实现了惰性解析。所谓惰性解析是指解析器在解析过程中,遇到函数声明,会跳过函数内部的代码。仅生成顶层代码的AST和字节码。
惰性解析的过程
关于惰性解析,我们可以结合下面这个例子来分析下:
function foo(a,b) {
var d = 100
var f = 10
return d + f + a + b;
}
var a = 1
var c = 4
foo(1, 5)
上面这段代码由上到下解析这段代码,在解析到foo函数,由于这是一个函数声明语句,V8这个阶段知只是将这个函数转换为函数对象,如图所示:
然后继续往下解析,由于后续的代码都是顶层代码,所以V8会为它们生成抽象语法树,最终生成的结果如下所示:
解析完成之后,由上至下执行代码,执行到foo函数的调用,过程是从foo函数对象中取出函数代码,然后和编译顶层代码一样,V8会先编译foo函数的代码,编译时同样需要先将其编译为抽象语法树和字节码,然后再解释执行。
拆解闭包——JavaScript的三个特性
JavaScript中的闭包有三个基础特性。
第一,JavaScript语言允许在函数内部定义新的函数,代码如下所示:
function foo() {
function inner() {
}
inner()
}
第二,可以在内部函数中访问父函数中定义的变量,代码如下所示:
var d = 20
//inner函数的父函数,词法作用域
function foo() {
var d = 55
//foo的内部函数
function inner() {
return d+2
}
inner()
}
第三,因为函数是一等公民,所以函数可以作为返回值,我们可以看下面这段代码:
function foo() {
return function inner(a, b) {
const c = a + b
return c
}
}
const f = foo()
以上就是和JavaScript闭包相关的三个重要特性:
可以在JavaScript函数内部定义新的函数;
内部函数中访问父函数中定义的变量;
因为JavaScript中的函数是一等公民,所以函数可以作为另外一个函数的返回值。
闭包给惰性解析带来的问题
function foo() {
var d = 20
return function inner(a, b) {
const c = a + b + d
return c
}
}
const f = foo()
按照通用的做法,d已经被v8销毁了,但是由于存活的函数inner依然引用了foo函数中的变量d,这样就会带来两个问题:
当foo执行结束时,变量d该不该被销毁?如果不应该被销毁,那么应该采用什么策略?
如果采用了惰性解析,那么当执行到foo函数时,V8只会解析foo函数,并不会解析内部的inner函数,那么这时候V8就不知道inner函数中是否引用了foo函数的变量d。
在执行foo函数的阶段,虽然采用了惰性解析,不会解析和执行foo函数中的inner函数,但是仍要判断inner函数是否引用了foo函数中的变量,这时候需要引入预解析器处理。
预解析器如何解决闭包所带来的问题 ?
V8引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析,其主要目的有两个。
第一,是判断当前函数是不是存在一些语法上的错误,如下面这段代码:
function foo(a, b) {
{/} //语法错误
}
var a = 1
var c = 4
foo(1, 5)
第二,除了检查语法错误之外,预解析器另外的一个重要的功能就是检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题。
此文章为5月Day18学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net