深入理解 JavaScript 执行机制:V8 引擎如何处理变量提升与作用域?(看完包会🚀)

160 阅读8分钟

作者:前端工程师
平台:稀土掘金
适用人群:初级~中级前端开发者,对 JS 执行机制有基础了解但想深入原理的同学


在日常开发中,你是否遇到过如下“诡异”现象?

js
编辑
console.log(myName); // undefined
var myName = "Mike";

或者:

js
编辑
showName(); // "函数showName被调用"
function showName() {
  console.log("函数showName被调用");
}

而换成 let 却直接报错?

js
编辑
console.log(hero); // ReferenceError: Cannot access 'hero' before initialization
let hero = "Superman";

这些行为的背后,其实是 JavaScript 的执行机制 在起作用。本文将结合 V8 引擎的工作流程,从编译阶段、执行上下文、变量提升等角度,系统性地解析 JS 是如何一步步执行代码的。


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

很多人说 JS 是“解释型语言”,但现代 JS 引擎(如 V8)早已不是一行行解释执行了。V8 实际上采用“即时编译”(JIT)策略:先快速编译,再优化执行。

JS 的执行分为两个关键阶段:

  • 编译阶段(Compilation Phase)
  • 执行阶段(Execution Phase)

这两个阶段几乎“无缝衔接”,但理解它们的差异,是掌握 JS 行为的关键。


二、编译阶段:变量提升的本质

2.1 什么是“变量提升”?

所谓“变量提升”,其实是 编译阶段对 var 和函数声明的预处理行为

来看这段经典代码:

js
编辑
console.log(myName); // undefined
var myName = "Mike";

V8 在编译阶段会做以下处理:

  1. 创建 全局执行上下文(Global Execution Context)
  2. 扫描代码,发现 var myName → 在变量环境中注册 myName = undefined
  3. 发现赋值语句 myName = "Mike" → 留到执行阶段处理

所以实际执行顺序等价于:

js
编辑
var myName;           // 编译阶段:提升并初始化为 undefined
console.log(myName);  // 执行阶段:输出 undefined
myName = "Mike";      // 执行阶段:赋值

关键点var 声明会被提升并初始化为 undefined;赋值不会提升。


2.2 函数声明 vs 函数表达式

函数声明具有 更高优先级,不仅提升名称,还提升整个函数体:

js
编辑
showName(); // "函数showName被调用"

function showName() {
  console.log("函数showName被调用");
}

编译后等价于:

js
编辑
function showName() { ... } // 整个函数被提升
showName(); // 正常调用

但如果是函数表达式(用 var/let 赋值):

js
编辑
func(); // TypeError: func is not a function
var func = function() {
  console.log('函数表达式');
};

因为 var func 只提升变量名(值为 undefined),函数体未被提升。

⚠️ 使用 let 定义函数表达式更危险:

js
编辑
func(); // ReferenceError
let func = () => {};

因为 let 存在 暂时性死区(TDZ) ,下文详述。


三、var vs let/const:提升机制的根本差异

特性varlet / const
是否提升是(提升到作用域顶部)是(但不初始化)
初始化值undefined不初始化(进入 TDZ)
重复声明允许(覆盖)报错
作用域函数作用域块级作用域

3.1 案例对比

js
编辑
console.log(a); // undefined
var a = 1;

console.log(b); // ReferenceError!
let b = 2;

为什么 let 报错?因为:

  • let b 在编译阶段被放入 词法环境(Lexical Environment)
  • 但在执行到 let b = 2 之前,b 处于 暂时性死区(Temporal Dead Zone, TDZ)
  • 任何访问都会抛出 ReferenceError

📌 TDZ 是 ES6 为避免变量未初始化就被使用而设计的安全机制


四、函数调用与执行上下文栈

4.1 调用栈(Call Stack)的工作流程

V8 使用 调用栈 管理函数执行:

  1. 全局代码 → 创建 全局执行上下文,压入栈底
  2. 调用函数 → 创建 函数执行上下文,压入栈顶
  3. 函数执行完毕 → 上下文出栈,内存回收

image.png

4.2 案例:参数、变量、函数的优先级

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

执行过程分析 重难点!!!

  1. 全局上下文:a = 1

  2. 调用 fn(3) → 创建函数上下文

    • 形参 a 接收实参 3
    • 编译阶段发现 var a → 但形参已存在,不会重复声明
    • 所以 a 初始值为 3
  3. 执行 console.log(a) → 输出 3

  4. 执行 var a = 2 → 赋值,a 变为 2

  5. 第二次 console.log(a) → 输出 2

✅ 输出结果:

3
2

💡 如果取消注释 function a() {},由于 函数声明优先级高于变量和参数a 会被覆盖为函数! 结果就会变成

[Function: a]
2

让我们总结一下 编译阶段的工作流程

  • 一段代码, 由V8引擎接管

    • JS调用栈来管理JS执行 以函数单位
    • 编译阶段
      • 创建一个全局执行上下文对象
        • 变量环境 a = underfined fn()
        • 词法环境 空
        • 可执行代码
  • fn(3) 执行阶段

    • 创建新的函数执行上下文对象入栈 (编译阶段)
      • 变量环境 函数是一等对象 函数声明更优先 a是参数

        1. a = underfined 因为var a 变量提升
        2. a = 3 因为参数给了a
        3. a = function 发现在该编译阶段中还有的函数声明 函数声明更优先

        b = undefined

        var 允许重复声明

      • 词法环境

      • 代码

  • fn 执行阶段

  • a function -> 2

    b = a = 2 (值拷贝)

    a =2(赋值)


五、实战:常见面试题解析

题目1:变量提升 + 函数提升

js
编辑
showName();
console.log(myName);
var myName = "Mike";
let hero = "Superman";
function showName() {
  console.log("函数showName被调用");
}

执行顺序

  1. 编译阶段:

    • 提升 function showName() → 可直接调用
    • 提升 var myName → myName = undefined
    • let hero 进入 TDZ(但未访问,不报错)
  2. 执行阶段:

    • showName() → 正常输出
    • console.log(myName) → undefined
    • 赋值 myName = "Mike"hero = "Superman"

✅ 输出:

text
编辑
函数showName被调用
undefined

题目2:重复声明

js
编辑
var a = 1;
var a = 2;
console.log(a); // 2

let b = 1;
// let b = 2; // SyntaxError: Identifier 'b' has already been declared
  • var 允许重复声明(后声明覆盖前声明)
  • let/const 不允许,这是块级作用域的重要保障

六、总结要点

概念说明
编译阶段V8 扫描代码,创建执行上下文,处理提升
变量提升var 提升并初始化为 undefinedlet/const 提升但不初始化(TDZ)
函数提升函数声明整体提升,优先级最高
执行上下文包含变量环境(Variable Environment)和词法环境(Lexical Environment)
调用栈管理函数执行顺序,先进后出(LIFO)
TDZlet/const 在声明前不可访问,防止意外使用未初始化变量

七、拓展思考

7.1 为什么设计 TDZ?

早期 var 的提升机制导致很多 bug(如意外覆盖、未定义使用)。ES6 引入 let/const 和 TDZ,强制开发者在声明后再使用变量,提升代码健壮性。

7.2 var 还能用吗?

  • 在现代项目中,推荐全部使用 let/const
  • var 仅用于兼容老代码或特殊场景(如全局挂载)

7.3 如何避免提升陷阱?

  • 始终在作用域顶部声明变量(即使使用 let
  • 启用 ESLint 规则:no-varno-use-before-define
  • 理解“提升”不是代码移动,而是编译行为

八、配图建议(供排版参考)

  1. 调用栈示意图:展示全局上下文 → 函数上下文入栈 → 出栈过程
  2. 执行上下文结构图:包含变量环境、词法环境、this 绑定等
  3. TDZ 示意图:用时间轴展示 let x 声明前后访问的区别

(注:掘金支持 Markdown 插图,可使用 Mermaid 或手绘图辅助说明)


九、总结🧠 :JavaScript 的执行机制核心要点

JavaScript 并非“边解释边执行”,而是采用 “编译 + 执行”交替进行 的模式。V8 引擎在代码真正运行前的一瞬间完成编译,这与 C++/Java 等传统编译型语言(先完整编译再执行)有本质区别。

1. 调用栈:V8 管理执行流程的核心数据结构

  • 编译总是在执行前发生:哪怕只有一行代码,V8 也会先快速编译,再执行。
  • 每个可执行单元(全局代码或函数)都会创建对应的执行上下文,并压入调用栈。
  • 函数执行完毕后,其执行上下文立即出栈并被销毁,相关变量随之进入垃圾回收流程。

2. 编译阶段的关键步骤(以函数为例)

当 V8 编译一段代码时,会按以下顺序构建执行上下文:

  1. 创建执行上下文对象
    包含变量环境(Variable Environment)和词法环境(Lexical Environment)。

  2. 处理参数与变量声明

    • var 声明的变量会被提升到变量环境,并初始化为 undefined
    • let / const 声明的变量会被放入词法环境,但处于暂时性死区(TDZ) ,直到执行到声明语句才可访问。
  3. 绑定实参与形参
    函数调用时,实参会覆盖形参的初始值(仅限函数上下文;全局上下文无此步骤)。

  4. 处理函数声明
    函数声明具有最高优先级:函数名作为标识符,其值为完整的函数体,并覆盖同名的变量或参数。


💡 一句话记住核心差异
var 是“提前报到但没上岗(undefined)”,
let/const 是“人到了但被锁在门口(TDZ)”,
函数声明则是“直接空降 CEO”。

结语

JavaScript 的执行机制看似简单,实则暗藏玄机。理解 V8 引擎如何处理编译与执行,不仅能帮你写出更可靠的代码,还能在面试中脱颖而出。

记住:JS 不是“一边解释一边执行”,而是“先编译,再执行” 。掌握这个底层逻辑,你就站在了进阶之路的起点。


欢迎点赞、收藏、评论交流!
如果你觉得有用,不妨分享给正在被“变量提升”困扰的小伙伴 👇