我了解了V8的预解析和全解析。由于lazying pasring,如果解析器遇到函数声明,它会跳过函数内部的代码,并且不会为其生成AST和字节码,而只生成顶层代码的AST和字节码。相反,当解析顶层代码遇到函数时,预解析器会对函数进行快速预解析。目的是:判断当前函数是否存在语法错误,如果发现,则将被抛出。检查函数内部是否引用外部变量。如果是,则将外部变量复制到堆中,下次执行函数时,直接使用堆中的引用。
以下面的代码为例,显然't'在它的[[Scopes]]头部有一个closure(a) {i: 1}(你可以在Chrome上运行这段代码),我的问题是'a '是一个三层嵌套函数,当'a'在全局环境中第一次预解析时,预解析器找到函数'a'中内部函数引用的变量的算法是什么,递归或通过其他方式?如果是递归的话,先找到函数b,然后扫描b里面的函数也就是函数c,c会不会再解析2次才真正被调用?
function a() {
let i = 1;
function b(){
function c() {
console.log(i);
}
return c;
}
return b;
}
let t = a();
console.dir(t);
我认为你的问题在https://v8.dev/blog/preparser#variable-allocation中得到了解答。
为了避免非线性性能开销,我们甚至在准备期间也执行全范围解析。我们存储了足够的元数据,以便以后可以简单地跳过内部函数,而不必重新准备它们。一种方法是存储内部函数引用的变量名。这存储起来很昂贵,并且需要我们仍然重复工作:我们已经在准备期间执行了变量解析。
相反,我们将变量分配的位置序列化为每个变量的密集标志数组。当我们延迟解析函数时,变量会按照预解析器看到的顺序重新创建,并且我们可以简单地将元数据应用于变量。
所以根据我的理解,在
a
的第一个“准备”中,它将找到变量i
,还将扫描b
和c
,并找到i
的用法,将其标记为堆分配。之后 a
可以在不进一步解析 b
和 c
的情况下执行,并且如果它们曾经被执行过,i
将已经在堆上分配并可以被内部函数使用。