JavaScript 执行机制详解:从 V8 引擎到调用栈

100 阅读4分钟

为什么 console.log(a)var a = 1 之前输出的是 undefined 而不是报错?
为什么 let 声明的变量不能提升?
函数声明和变量声明谁优先?
JS 是如何一步步执行代码的?

这些问题的答案,都藏在 JavaScript 的执行机制 中。本文将带你从底层理解 JS 是如何被浏览器(以 Chrome 的 V8 引擎为例)编译和执行的。


一、JS 不是“解释型”那么简单

很多人说 JavaScript 是“解释型语言”,但现代 JS 引擎(如 V8)早已采用 即时编译(JIT, Just-In-Time Compilation) 技术——即 先编译,再执行

虽然不像 Java 或 C++ 那样提前编译成字节码或机器码,但 JS 在执行前会经历一个极快的 编译阶段,这个阶段发生在执行的“一霎那”。


二、JS 执行的两个核心阶段

1. 编译阶段(Compilation Phase)

  • 目的:为后续执行做准备。

  • 主要工作

    • 检查语法错误(Syntax Error)
    • 创建 执行上下文(Execution Context)
    • 进行 变量提升(Hoisting)

执行上下文包含什么?

每个执行上下文包含两个关键环境:

环境类型存放内容
变量环境(Variable Environment)var 声明的变量、函数声明
词法环境(Lexical Environment)letconst 声明的变量(处于“暂时性死区”)

📌 注意:函数声明会被完整提升(包括函数体),而 var 只提升变量名并初始化为 undefinedlet/const 虽然也被“提升”,但不会初始化,在声明前访问会报错(TDZ)。


2. 执行阶段(Execution Phase)

  • 按照代码书写顺序,逐行执行。
  • 此时变量已被分配内存,函数可被调用。
  • 如果遇到函数调用,会创建新的执行上下文,并压入 调用栈(Call Stack)

三、调用栈:JS 执行的“调度中心”

V8 引擎使用 调用栈 来管理函数的执行顺序。

  • 全局执行上下文 最先被压入栈底。
  • 每调用一个函数,就创建一个新的执行上下文,压入栈顶。
  • 函数执行完毕后,其上下文出栈,相关变量可能被垃圾回收。

✅ 栈的特点:后进先出(LIFO)。这保证了函数调用的嵌套逻辑正确执行。


四、变量提升详解:var vs let/const

示例 1:var 的提升

console.log(myName); // undefined
var myName = '陈倩文';

编译阶段

  • var myName; → 提升到顶部,初始化为 undefined
  • 执行阶段:myName = '陈倩文' 赋值

示例 2:函数声明优先级更高

showName(); // "函数showName被执行"
console.log(myName); // undefined

var myName = '陈倩文';
function showName() {
  console.log('函数showName被执行');
}

编译阶段处理顺序

  1. 函数声明 showName 被完整提升(优先于变量)
  2. var myName 被提升为 undefined

💡 规则:函数声明 > var 变量声明

示例 3:let 的暂时性死区(TDZ)

console.log(hero); // ReferenceError!
let hero = '钢铁侠';
  • let hero 在编译阶段被放入 词法环境,但未初始化。
  • let 声明前访问 → 处于 暂时性死区(Temporal Dead Zone) → 报错!

五、函数执行上下文的创建过程

以如下函数为例:

var a = 1;

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

fn(3);

全局上下文(编译阶段):

  • var aa = undefined
  • function fn → 完整提升

调用 fn(3) 时(执行阶段):

  1. 创建 fn 的执行上下文,压入调用栈

  2. 编译阶段(函数内部)

    • 形参 a 被设为 3
    • var a 声明 → 与形参同名,覆盖形参(但值仍为 3,直到赋值)
    • var bb = undefined
  3. 执行阶段

    • var a = 2a 被赋值为 2
    • var b = ab = 2
    • 输出 2

⚠️ 注意:var 允许重复声明,不会报错,但会覆盖(或忽略)。


六、var vs let/const 对比总结

特性varlet / const
作用域函数作用域块级作用域 {}
提升提升并初始化为 undefined提升但不初始化(TDZ)
重复声明允许(静默忽略)不允许(SyntaxError)
全局对象属性是(如 window.a

七、值类型 vs 引用类型:内存模型补充

虽然不属于执行机制主线,但常被混淆,顺带说明:

// 基本类型(值拷贝)
let str = 'hello';
let str2 = str;
str2 = '你好';
console.log(str, str2); // 'hello' '你好'

// 引用类型(地址拷贝)
let obj = { name: '郑老板' };
let obj2 = obj;
obj2.name = '郑总';
console.log(obj.name); // '郑总'
  • 基本类型(string, number, boolean 等):存储在 栈内存,赋值是值拷贝。
  • 引用类型(object, array, function):实际数据存在 堆内存,变量保存的是 地址引用

八、总结:JS 执行流程全景图

  1. 代码加载 → V8 引擎接管

  2. 编译阶段(极快):

    • 创建全局执行上下文
    • 语法检查
    • 变量/函数提升(按规则)
  3. 执行阶段

    • 从上到下执行代码
    • 遇到函数调用 → 创建新上下文 → 压入调用栈
    • 函数执行完 → 上下文出栈 → 内存回收
  4. 全程依赖调用栈管理执行顺序

✅ 关键记忆点:

  • 先编译,后执行
  • 函数声明 > var > let/const(TDZ)
  • 调用栈 = 执行上下文的容器
  • var 有坑,let/const 更安全

掌握这套机制,不仅能解释各种“诡异”的 JS 行为,还能写出更可靠、可维护的代码。建议多结合调试工具(如 Chrome DevTools 的断点)观察调用栈变化,加深理解。


如有需要,我还可以提供配套练习题或可视化执行流程图!