深入浅出 JavaScript 执行机制:V8 引擎如何运行你的代码

178 阅读5分钟

作为前端开发者,我们几乎每天都在与 JavaScript 打交道,但你是否真正了解它是如何被执行的?今天我们就来深入探讨 JavaScript 的执行机制,揭开 V8 引擎运行代码的神秘面纱。

从一段令人困惑的代码开始

先来看一个简单的例子,你能准确说出它的执行结果吗?

// 1.js
showName()
console.log(myName)
console.log(hero)

var myName = 'zhangsan'
let hero = '钢铁侠'

function showName(){
  console.log('函数showName被执行')
}

如果你认为这会报错,那么你可能需要重新认识 JavaScript 的执行机制了。实际上,这段代码会正常执行,输出:

函数showName被执行
undefined
Uncaught ReferenceError: Cannot access 'hero' before initialization

为什么会出现这样的结果?这就涉及到 JavaScript 执行的两个关键阶段:编译阶段和执行阶段。

V8 引擎的两阶段执行模型

编译阶段:代码执行前的准备工作

JavaScript 虽然是解释型语言,但在执行前会经历一个短暂的编译阶段。这个阶段主要完成三件事:

  1. 语法检测:检查代码是否存在语法错误
  2. 变量提升:处理变量和函数的声明
  3. 生成执行上下文:为代码执行准备环境

在编译阶段,V8 引擎会重新组织我们的代码。对于上面的 1.js,编译后的代码可以理解为:

// 编译后的 1.js
var myName; // var 只提升声明,不提升赋值
function showName(){ // 函数声明整体提升
  console.log('函数showName被执行')
}

// 可执行代码
showName()
console.log(myName)
console.log(hero)
myName = 'zhangsan'
let hero = '钢铁侠' // let 存在暂时性死区

这里有几个关键点需要注意:

· var 声明提升:使用 var 声明的变量会被提升到作用域顶部,但只提升声明,不提升赋值 · 函数声明提升:函数声明会整体提升,且优先级高于变量声明 · let/const 的暂时性死区:使用 let 或 const 声明的变量也存在提升,但在声明前访问会报错

执行阶段:逐行执行代码

编译阶段完成后,V8 引擎开始逐行执行代码:

  1. 执行 showName(),输出"函数showName被执行"
  2. 执行 console.log(myName),此时 myName 为 undefined
  3. 执行 console.log(hero),此时 hero 处于暂时性死区,抛出错误

深入理解执行上下文和调用栈

要真正理解 JavaScript 的执行机制,我们需要了解两个核心概念:执行上下文和调用栈。

执行上下文:代码的执行环境

每当 JavaScript 执行一段代码时,都会创建一个执行上下文。执行上下文包含三个重要部分:

· 变量环境:存储 var 和函数声明 · 词法环境:存储 let 和 const 声明 · 可执行代码:实际的代码逻辑

调用栈:管理执行顺序的数据结构

调用栈是一种栈数据结构,负责管理执行上下文的创建和销毁:

  1. 首先,全局执行上下文被压入调用栈
  2. 当函数被调用时,创建新的函数执行上下文并压入栈顶
  3. 只有栈顶的执行上下文可以执行
  4. 函数执行完毕后,其执行上下文从栈中弹出

让我们通过另一个例子来理解这个过程:

// 3.js
var a = 1;
function fn(a){
  var a = 2;
  function a(){}
  var b = a;
  console.log(a)
}
fn(3)

这段代码的执行过程如下:

编译阶段(全局):

  1. 创建全局执行上下文
  2. 变量环境中:a = undefined, fn = function(){...}
  3. 词法环境:为空(没有 let/const)
  4. 可执行代码:a = 1; fn(3);

执行阶段(全局):

  1. 执行 a = 1,全局变量 a 被赋值为 1
  2. 执行 fn(3),创建函数执行上下文

编译阶段(函数 fn):

  1. 创建函数执行上下文
  2. 形参 a 被添加到变量环境,初始值为 3
  3. 处理函数内部声明: · 函数声明 function a(){} 提升,覆盖参数 a · var a 重复声明,被忽略 · var b 声明提升,值为 undefined
  4. 变量环境中:a = function a(){}, b = undefined
  5. 词法环境:为空

执行阶段(函数 fn):

  1. 执行 a = 2,变量环境中的 a 被重新赋值为 2
  2. 执行 b = a,b 被赋值为 2
  3. 执行 console.log(a),输出 2

最终,这段代码输出 2,而不是你可能预期的 3 或其他值。

函数表达式 vs 函数声明

一个重要但常被忽视的区别是函数表达式和函数声明的不同行为:

// 函数声明 - 会提升
funcDeclaration() // 正常工作
function funcDeclaration() {
  console.log('函数声明会提升')
}

// 函数表达式 - 不会提升
funcExpression() // 报错:funcExpression is not a function
var funcExpression = function() {
  console.log('函数表达式不会提升')
}

// 使用 let/const 的函数表达式 - 不会提升且存在暂时性死区
funcLet() // 报错:Cannot access 'funcLet' before initialization
let funcLet = () => {
  console.log('使用let的函数表达式')
}

JavaScript 执行机制总结

通过上面的分析,我们可以总结出 JavaScript 的执行机制:

  1. 先编译后执行:JavaScript 代码在执行前会经历一个短暂的编译阶段
  2. 执行上下文:每段代码都在特定的执行上下文中运行,包含变量环境、词法环境和可执行代码
  3. 调用栈管理:V8 使用调用栈来管理执行上下文的创建和销毁
  4. 变量提升差异: · var 和函数声明会提升 · let 和 const 存在暂时性死区 · 函数表达式不会提升

理解 JavaScript 的执行机制对于编写高质量代码和调试复杂问题至关重要。下次当你遇到令人困惑的 JavaScript 行为时,不妨回想一下编译阶段、执行上下文和调用栈,或许就能找到答案。

希望这篇文章能帮助你更好地理解 JavaScript 的执行机制。如果你有任何疑问或想法,欢迎在评论区交流讨论!