震惊:v8引擎竟是如此操作代码(JS预编译)

422 阅读5分钟

JavaScript 的执行流程包含了编译阶段和执行阶段,理解这两者对于掌握 JavaScript 的工作原理至关重要。在本文中,我们将通过 V8 引擎如何执行代码的具体示例,来帮助你清晰地理解编译和执行的先后顺序,特别是如何进行预编译(Hoisting)操作。

05CF44C8.jpg

1. V8 引擎的编译与执行流程

V8 引擎的工作分为两个主要阶段:

  • 编译阶段:V8 首先将 JavaScript 代码解析成抽象语法树(AST),并进行即时编译(JIT)转换为机器码。这一阶段主要是为执行代码做准备。
  • 执行阶段:在字节码或机器码生成后,V8 开始执行 JavaScript 代码。在执行过程中,涉及到变量和函数的赋值、调用等。

2. 作用域提升(Hoisting)

05D0F363.gif 作用域提升是指在编译阶段,JavaScript 会将变量声明(var)和函数声明提前到当前作用域的顶部。这意味着,在代码执行之前,JavaScript 已经知道了变量和函数的名字,但未初始化的变量会被赋值为 undefined,函数会直接指向函数体。

下面的代码展示了这一过程:

function fn(a) {
  console.log(a);  // function a() {}
  var a = 123
  console.log(a);  // 123
  function a() {}
  var b = function() {}
  console.log(b);  // function() {}
  function c() {}
  var c = a
  console.log(c);  // 123
}
fn(1);

解释:

  • 执行上下文(Execution Context) :当 fn 被调用时,V8 引擎会为该函数创建一个执行上下文(EC)。在函数被编译时,abc 等变量和函数会被提升到当前执行上下文的顶部。
  • AO(Activation Object)fn 函数内的所有声明(如变量和函数)会被存储在 AO 对象中。图中的注释部分展示了函数体被预编译后的 AO 结构:
AO = {
  a: undefined  //1  function a() {}  123,
  b: undefined  //function() {},
  c: undefined  //function c() {}, 123
}

代码行为分析:

阶段一:编译阶段(Parsing and Compilation)

V8 引擎在执行函数 fn 之前,会先进行编译处理,这时会发生作用域提升:

在编译阶段,V8 会按以下顺序处理:

  1. 解析形参:a 被识别为函数参数,在作用域中创建标识符 a
  2. 处理函数声明:优先处理所有函数声明 · function a() {} → 提升,覆盖形参 a(现在 a 指向函数) · function c() {} → 提升,c 指向函数
  3. 处理变量声明:处理 var 声明(但只声明不赋值) · var a → 由于 a 已存在(现在是函数),所以忽略声明 · var b → b 被创建,初始值为 undefined · var c → 由于 c 已存在(现在是函数),所以忽略声明 阶段二:执行阶段(Execution)

假设调用 fn() 不传参:

function fn(a) {
  // 执行开始时的状态(编译后):
  // a = function a() {}
  // b = undefined
  // c = function c() {}
  
  // 第1行:
  console.log(a)
  // 输出:function a() {}
  
  // 第2行:
  var a = 123
  // 注意:var a 在编译阶段已处理,这里只是赋值操作
  // a 被重新赋值为 123
  
  // 第3行:
  console.log(a)
  // 输出:123
  
  // 第4行:
  function a() {}
  // 这是函数声明,在编译阶段已处理,执行阶段不执行任何操作
  
  // 第5行:
  var b = function() {}
  // 函数表达式,将匿名函数赋值给 b
  // b 现在指向匿名函数
  
  // 第6行:
  console.log(b)
  // 输出:function() {}
  
  // 第7行:
  function c() {}
  // 函数声明,编译阶段已处理,执行阶段无操作
  
  // 第8行:
  var c = a
  // 将 c 赋值为当前 a 的值(123)
  // c 从函数变成了数字 123
  
  // 第9行:
  console.log(c)
  // 输出:123
}

如果传递参数 fn(456) 呢?

fn(456);

function fn(a) {
  // 编译阶段:
  // 1. 形参 a = 456(传入的值)
  // 2. 函数声明 function a() {} 提升 → 覆盖形参 a
  // 所以执行开始时 a 是 function a() {}
  
  console.log(a);  // 仍然是 function a() {},不是 456
  // ... 后续相同
}

3. 全局执行上下文与局部执行上下文

05D0BC45.gif 除了函数内部的执行上下文外,全局执行上下文也遵循相同的编译与执行规则。全局上下文创建时,V8 引擎会创建一个 全局对象(如 windowglobal),并将全局变量和函数声明提升到该对象上。

例如:

var a = 10;
function foo() {
  console.log(a);  // undefined
  var a = 20;
}
foo();
console.log(a);  // 10

解释:

  • 在全局作用域,var a 被提升,初始化为 undefined
  • foo 函数内,var a 同样会被提升,初始化为 undefined。因此,函数内的 console.log(a) 输出 undefined
  • 最后,console.log(a) 输出的是全局作用域中的 a,其值为 10因为输出在赋值之前,所以v8引擎在函数作用域中找不到值,便会由内而外的去全局作用域找到 var a = 10;,从而输出‘10’.

4. 总结

05D1BC60.jpg 通过上述代码示例,我们可以看到 V8 引擎如何在编译阶段进行作用域提升,并通过执行上下文管理变量和函数的声明。所以总结一下声明顺序

  1. 函数声明优先级最高:函数声明会覆盖同名的形参和变量声明
  2. var 声明的变量会提升但只提升声明:var b 提升,但 b = function() {} 不会提升
  3. 执行顺序决定最终值:虽然编译阶段做了提升,但执行阶段的赋值操作会改变变量值
  4. 同名处理规则: · 函数声明 > 形参 > 变量声明 · 后面的赋值会覆盖前面的声明

主要要点:

  • 编译与执行:V8 引擎首先编译代码,再执行字节码。
  • 作用域提升(Hoisting) :变量和函数声明会被提升,但赋值会按顺序执行。
  • 执行上下文(EC) :每个函数的执行都会创建一个执行上下文,并在编译阶段完成预编译(提升)。
  • 全局与局部作用域:全局作用域和局部作用域会有不同的提升规则,但基本遵循相同的原理。

通过掌握这些基础知识,我们可以更轻松地理解 JavaScript 代码的执行顺序,并避免一些潜在的陷阱,好的姐妹们,咱们下期再见。

05D26513.jpg