V8 是如何实现 JavaScript Hoist 的

1 首先

今天在知乎上看到一个问题“JavaScript有预编译吗?”,题主实际上是对 JavaScript 变量提升(hoist)机制的实现过程有疑惑。我刚知道 hoist 时也好奇过浏览器是怎么实现的,就跑去看了一下 V8 引擎的源码,做了一些笔记,现在正好趁机整理出来。

2 Hoist

var 和 function 的 hoist 是老生常谈的问题,网上有大量资料,JavaScript 秘密花园

3 V8 源码

墙外:Chromium
墙内:GitHub

4 V8 的 Hoist

V8 中变量提升涉及到两个步骤,Parse 和 Analyze 。先 Parse 一遍代码得出 AST (抽象语法树)等信息,再把信息 Analyze 一遍。虽然 V8 有“预语法分析”,「preparser.h」, 只是为了收集信息辅助后续加速的,不属于预编译。

Talk is cheap, let me show you the code.

5 Parse 部分

在「parser.cc」里

var 声明的处理在 Parser::ParseVariableDeclarations 函数中,对于 var a = 2,V8 是将声明和赋值分开处理的,即转换为 var a; a = 2; ,然后前者由 Parser::Declare 提升。提升就是将每一层作用域用户声明的变量都放在该层的 variables_ 表中。

 1 Block* Parser::ParseVariableDeclarations( /*...*/ ) {
 2   /*...*/
 3   // 一层层地向外找合适的声明作用域
 4   Scope* declaration_scope = DeclarationScope(mode);
 5   /*...*/
 6   // 分析出变量名
 7   name = ParseIdentifier(kDontAllowEvalOrArguments, CHECK_OK);
 8   /*...*/
 9   // 下面会详细讲 proxy
10   VariableProxy* proxy = NewUnresolved(name, mode);
11   // 进一步封装
12   Declaration* declaration =
13       factory()->NewVariableDeclaration(proxy, mode, scope_, pos);
14   // 声明变量,注意 VAR 声明时第二个参数是 false
15   Variable* var = Declare(declaration, mode != VAR, CHECK_OK);
16   /*...*/
17   // 如果接下来还有等号的话(带赋值的变量声明)
18   if (peek() == Token::ASSIGN /*...*/ ) {
19     /*...*/
20     // 解析出待赋的值
21     value = ParseAssignmentExpression(var_context != kForStatement, CHECK_OK);
22     /*...*/
23   }
24   /*...*/
25   // 按正常的赋值表达式语句解析
26   VariableProxy* proxy = initialization_scope->NewUnresolved(factory(), name);
27     Assignment* assignment = factory()->NewAssignment(init_op, proxy, value, pos);
28     block->AddStatement(
29         factory()->NewExpressionStatement(assignment, RelocInfo::kNoPosition),
30         zone());
31   /*...*/
32 }

 

function 声明在 Parser::ParseFunctionDeclaration 中,

Statement* Parser::ParseFunctionDeclaration( /*...*/ ) {
  /*...*/
  // 可以看到函数体在这里也一并读取了
  FunctionLiteral* fun = ParseFunctionLiteral( /*...*/ );
  /*...*/
  // 与var 声明大同小异
  VariableProxy* proxy = NewUnresolved(name, mode);
  Declaration* declaration =
      factory()->NewFunctionDeclaration(proxy, mode, fun, scope_, pos);
  // 注意这里的 true
  Declare(declaration, true, CHECK_OK);
  /*...*/
}

 

注意两者传入 Parser::Declare 函数的第二个参数值(resolve)不一样。

前面提过,对于“var a = 2”,V8 是将声明和赋值分开处理的,即转换为 var a; a = 2; 。var a; 提升后, a = 2; 还在原来的位置,相当于拆散了。

在 ES5 中,除了 function 作用域外还有 with 和 catch 可以产生作用域的(最近也做了笔记)。而 var 的 hoist 是提升到函数作用域的最前面,所以会有下面的情况:

(function fn() {
  try {throw 1} catch(a) {
    console.log(a); // 1 ,这里的 a 是 catch 的
    var a = 3; // 这里的 a 也是 catch 的
  }
  console.log(a); // undefined ,这里的 a 是 fn 的
}());

所以对于 var 的声明,不可以在读到 var a = 3; 时就绑定,于是 V8 的 proxy 变量代理的机制就显得十分有用了。

对于 var 声明, proxy 不跟变量绑定,而是在 Parse 一遍后的 Analyze 阶段再统一进行绑定,所以传入 false;而由于函数没有上面的情况,在 Parse 的时候就可以将其 proxy 与变量绑定,于是传入的 resolve 为 true。

看看 Parser::Declare 如何处理变量:

 1 Variable* Parser::Declare(Declaration* declaration, bool resolve, bool* ok) {
 2   VariableProxy* proxy = declaration->proxy();
 3   DCHECK(proxy->raw_name() != NULL);
 4   const AstRawString* name = proxy->raw_name();
 5   VariableMode mode = declaration->mode();
 6   // 层层向外找最近的声明作用域
 7   Scope* declaration_scope = DeclarationScope(mode);
 8   // 只有符合以下几个声明作用域才会进行注册
 9   if (declaration_scope->is_function_scope() ||
10       declaration_scope->is_strict_eval_scope() ||
11       declaration_scope->is_block_scope() ||
12       declaration_scope->is_module_scope() ||
13       declaration_scope->is_script_scope()) {
14     // 看看有没有被注册了
15     var = declaration_scope->LookupLocal(name);
16     // 如果没有就新注册一个
17     if (var == NULL) {
18       // var 和 function 还有 ES6 的 let const 等等都会在这里做标记
19       var = declaration_scope->DeclareLocal(
20           name, mode, declaration->initialization(),
21           declaration->IsFunctionDeclaration() ? Variable::FUNCTION
22                                                : Variable::NORMAL,
23           kNotAssigned);
24     } else if ( /*...*/ ) {
25       /*...*/ // ES6 相关
26     } else if (mode == VAR) {
27       // 如果之前已经注册了的话,就可能被赋过值了
28       var->set_maybe_assigned();
29     }
30   }
31   /*...*/
32   // 对于每次声明 V8 都会挂载一个 declaration 节点
33   // 虽然只有在必要的时候才会生成相应代码
34   // 但对于一个变量有过多的 declaration 节点必然会影响查找性能
35   // 所以尽量不要重复声明
36   declaration_scope->AddDeclaration(declaration);
37   /*...*/
38   // 前面提到的传入的 resolve 
39   // function 为 true
40   // var 为 false
41   if (resolve && var != NULL) {
42     // 绑定在一起
43     proxy->BindTo(var);
44   }
45   return var;
46 }

 

6 Analyze 部分

在「scope.cc」里

Analyze 是由最外层 Scope::Analyze 开始,一层层向里递归的查看需要 resolve 的作用域,然后对该作用域的每一个需要 resolve 的 proxy 再一层层向外递归查找最近的同名变量进行绑定。

7 总结

V8 为了处理声明提升,在每层作用域都会维护一个独立的声明作用域(variables_ 表),这样运行时就可以从声明作用域中递归查找变量。为了处理一些不能确定的特殊情况,V8 会将 proxy 与变量的绑定推迟到 Analyze 阶段。

所以对 var 和 function 提升的处理在语法分析阶段就搞定了,不需要预编译。

 

转自:https://blog.crimx.com/2015/03/29/javascript-hoist-under-the-hood/

 

posted @ 2017-10-20 15:58  wanghuohuo  阅读(395)  评论(0编辑  收藏  举报