首先
今天在知乎上看到一个问题“JavaScript有预编译吗? ”,题主实际上是对 JavaScript 变量提升(hoist)机制的实现过程有疑惑。我刚知道 hoist 时也好奇过浏览器是怎么实现的,就跑去看了一下 V8 引擎的源码,做了一些笔记,现在正好趁机整理出来。
Hoist
var 和 function 的 hoist 是老生常谈的问题,网上有大量资料,JavaScript 秘密花园
V8 源码
墙外:Chromium 墙内:GitHub
V8 的 Hoist
V8 中变量提升涉及到两个步骤,Parse 和 Analyze 。先 Parse 一遍代码得出 AST (抽象语法树)等信息,再把信息 Analyze 一遍。虽然 V8 有“预语法分析”,「preparser.h」, 只是为了收集信息辅助后续加速的,不属于预编译。
Talk is cheap, let me show you the code.
Parse 部分
在「parser.cc
」里
var 声明的处理在 Parser::ParseVariableDeclarations
函数中,对于 var a = 2
,V8 是将声明和赋值分开处理的,即转换为 var a; a = 2;
,然后前者由 Parser::Declare
提升。提升就是将每一层作用域用户声明的变量都放在该层的 variables_
表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Block* Parser::ParseVariableDeclarations( ) { Scope* declaration_scope = DeclarationScope(mode); name = ParseIdentifier(kDontAllowEvalOrArguments, CHECK_OK); VariableProxy* proxy = NewUnresolved(name, mode); Declaration* declaration = factory()->NewVariableDeclaration(proxy, mode, scope_, pos); Variable* var = Declare(declaration, mode != VAR, CHECK_OK); if (peek() == Token::ASSIGN ) { value = ParseAssignmentExpression(var_context != kForStatement, CHECK_OK); } VariableProxy* proxy = initialization_scope->NewUnresolved(factory(), name); Assignment* assignment = factory()->NewAssignment(init_op, proxy, value, pos); block->AddStatement( factory()->NewExpressionStatement(assignment, RelocInfo::kNoPosition), zone()); }
function 声明在 Parser::ParseFunctionDeclaration
中,
1 2 3 4 5 6 7 8 9 10 11 12 13
Statement* Parser::ParseFunctionDeclaration( ) { FunctionLiteral* fun = ParseFunctionLiteral( ); VariableProxy* proxy = NewUnresolved(name, mode); Declaration* declaration = factory()->NewFunctionDeclaration(proxy, mode, fun, scope_, pos); 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 是提升到函数作用域的最前面,所以会有下面的情况:
1 2 3 4 5 6 7 8 9 10
(function fn ( ) { try {throw 1 } catch (a) { console .log(a); var a = 3 ; } console .log(a); }());
所以对于 var 的声明,不可以在读到 var a = 3;
时就绑定,于是 V8 的 proxy 变量代理的机制就显得十分有用了。
对于 var 声明, proxy 不跟变量绑定,而是在 Parse 一遍后的 Analyze 阶段再统一进行绑定,所以传入 false;而由于函数没有上面的情况,在 Parse 的时候就可以将其 proxy 与变量绑定,于是传入的 resolve 为 true。
看看 Parser::Declare
如何处理变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
Variable* Parser::Declare(Declaration* declaration, bool resolve, bool * ok) { VariableProxy* proxy = declaration->proxy(); DCHECK(proxy->raw_name() != NULL ); const AstRawString* name = proxy->raw_name(); VariableMode mode = declaration->mode(); Scope* declaration_scope = DeclarationScope(mode); if (declaration_scope->is_function_scope() || declaration_scope->is_strict_eval_scope() || declaration_scope->is_block_scope() || declaration_scope->is_module_scope() || declaration_scope->is_script_scope()) { var = declaration_scope->LookupLocal(name); if (var == NULL ) { var = declaration_scope->DeclareLocal( name, mode, declaration->initialization(), declaration->IsFunctionDeclaration() ? Variable::FUNCTION : Variable::NORMAL, kNotAssigned); } else if ( ) { } else if (mode == VAR) { var->set_maybe_assigned(); } } declaration_scope->AddDeclaration(declaration); if (resolve && var != NULL ) { proxy->BindTo(var); } return var; }
Analyze 部分
在「scope.cc
」里
Analyze 是由最外层 Scope::Analyze
开始,一层层向里递归的查看需要 resolve 的作用域,然后对该作用域的每一个需要 resolve 的 proxy 再一层层向外递归查找最近的同名变量进行绑定。
总结
V8 为了处理声明提升,在每层作用域都会维护一个独立的声明作用域(variables_
表),这样运行时就可以从声明作用域中递归查找变量。为了处理一些不能确定的特殊情况,V8 会将 proxy 与变量的绑定推迟到 Analyze 阶段。
所以对 var 和 function 提升的处理在语法分析阶段就搞定了,不需要预编译。
【完】