前言:为什么你需要彻底掌握 JS 执行机制?
JavaScript 作为一门"看似简单"的脚本语言,其底层执行机制却异常精巧。许多开发者在日常开发中能写出功能正确的代码,但一旦遇到变量提升、作用域链、闭包、暂时性死区(TDZ)、函数声明优先级等问题时,往往陷入困惑。究其根本,是对 JavaScript 的执行机制缺乏系统性理解。
与 C++、Java 等编译型语言不同,JavaScript 是一种 即时编译(JIT)语言 ,由 V8 引擎(Chrome 和 Node.js 使用)负责在运行时动态编译和执行。这种"一边解析、一边编译、一边执行"的特性,使得 JS 的执行过程分为清晰的 三个阶段 :编译前准备 → 编译阶段 → 执行阶段。
本文将深入剖析 JavaScript 的执行机制,重点讲解:
- V8 引擎如何处理一段 JS 代码
- 执行上下文(Execution Context)的创建与结构
- 变量环境(Variable Environment)与词法环境(Lexical Environment)的区别
var、let、const在编译阶段的行为差异- 调用栈(Call Stack)如何管理函数执行
- 结合真实面试题,巩固核心概念
无论你是前端初学者,还是准备大厂面试的资深工程师,掌握这些底层原理,都将极大提升你对 JavaScript 的掌控力。
一、JavaScript 执行的三大环节
1. 编译前准备:V8 引擎接管代码
当你在浏览器中写下一串 JavaScript 代码:
arduino
js
编辑
console.log("Hello World");
这段代码并不会立即执行。首先,它会被 V8 引擎接管 。V8 是 Google 开发的高性能 JavaScript 引擎,采用 即时编译(Just-In-Time Compilation, JIT) 技术,将 JS 代码转换为机器码执行。
📌 关键点 :JS 虽然是"解释型语言",但现代引擎(如 V8)早已不是逐行解释,而是 先编译再执行。
2. 编译阶段(Creation Phase):构建执行上下文
这是 JS 执行中最容易被忽视、却最关键的一环。编译发生在执行前的一瞬间,V8 引擎会做以下几件事:
✅ 步骤 1:创建执行上下文对象(Execution Context)
每个可执行代码块(全局代码或函数)都会创建一个 执行上下文对象,它包含三个核心部分:
| 组件 | 作用 |
|---|---|
| 变量环境(Variable Environment) | 存储 var 声明的变量、函数声明 |
| 词法环境(Lexical Environment) | 存储 let、const 声明的变量、块级作用域绑定 |
| 可执行代码(Code to Execute) | 待执行的语句 |
💡 执行上下文 = 变量环境 + 词法环境 + 可执行代码
✅ 步骤 2:变量提升(Hoisting)与初始化
var声明的变量 :被提升到变量环境,初始值为undefined- 函数声明(
function fn() {}) :被提升到变量环境,值为函数体(优先级高于var) let/const声明的变量 :不会被提升到变量环境 ,而是放入 词法环境 ,并处于 暂时性死区(TDZ) ,直到赋值语句执行
✅ 步骤 3:参数绑定(仅函数上下文)
- 函数参数被视为
var声明的变量,放入变量环境 - 实参与形参匹配,未传入的参数值为
undefined
✅ 步骤 4:处理重复声明
var a; var a;→ 合法,第二个声明被忽略let b; let b;→ 报错(SyntaxError),因为let不允许重复声明
3. 执行阶段(Execution Phase):逐行运行代码
编译完成后,V8 引擎开始 逐行执行代码:
- 读取/修改变量环境和词法环境中的值
- 调用函数 → 创建新的执行上下文 → 压入调用栈
- 遇到
return或函数结束 → 弹出执行上下文 → 触发垃圾回收
⚠️ 注意 :函数没有
return语句时,默认返回undefined,与作用域销毁无关。
二、执行上下文的核心:变量环境 vs 词法环境
这是理解 JS 作用域机制的基石。
🔹 变量环境(Variable Environment)
-
存储内容:
var声明的变量- 函数声明(
function fn() {}) - 函数参数
-
特点:
- 支持变量提升(Hoisting)
- 在整个函数作用域内有效
- 重复
var声明不会报错
🔹 词法环境(Lexical Environment)
-
存储内容:
let、const声明的变量- 块级作用域(
{})中的绑定
-
特点:
- 不支持变量提升
- 存在 暂时性死区(TDZ) :声明前访问会报
ReferenceError - 支持块级作用域
- 重复声明会报错
🌰 示例对比
ini
js
编辑
console.log(a); // undefined(var 提升)
console.log(b); // ReferenceError(let 处于 TDZ)
var a = 1;
let b = 2;
三、调用栈(Call Stack):函数执行的调度器
JavaScript 是单线程语言,通过 调用栈 管理函数执行顺序:
- 全局上下文 首先被压入栈底
- 每调用一个函数,就创建其执行上下文并 压入栈顶
- 函数执行完毕,上下文 弹出栈,内存被回收
- 栈为空时,程序结束
scss
js
编辑
function a() { b(); }
function b() { c(); }
function c() { console.log("Done"); }
a(); // 调用栈:[global] → [a] → [b] → [c]
💡 栈溢出(Stack Overflow) :递归过深导致调用栈超出内存限制。
四、经典案例深度解析
案例 1:函数声明 vs 变量声明优先级
ini
js
编辑
function fn(s) {
var s = 2;
function s() { };
var b = s;
console.log(b); // 2
console.log(s); // 2
}
fn(3);
编译阶段:
- 参数
s被视为var s - 函数声明
function s()覆盖 参数s,s成为函数 var s = 2将s重新赋值为数字 2
执行结果 :b = s = 2
案例 2:var vs let 重复声明
ini
js
编辑
var a = 1;
var a = 2; // 合法,a = 2
let b = 3;
let b = 4; // SyntaxError: Identifier 'b' has already been declared
原因:
var在变量环境中允许多次声明let在词法环境中禁止重复绑定
五、大厂高频面试题
❓ 1. 以下代码输出什么?为什么?
ini
js
编辑
console.log(a);
var a = 1;
console.log(b);
let b = 2;
答案:
javascript
text
编辑
undefined
ReferenceError: Cannot access 'b' before initialization
解析:
var a被提升,初始值undefinedlet b处于 TDZ,访问时报错
❓ 2. 解释 var、let、const 的区别(从编译角度)
参考答案:
var:编译阶段提升到 变量环境 ,初始值undefined,函数作用域let/const:编译阶段放入 词法环境,存在 TDZ,块级作用域const必须初始化,且不能重新赋值(但对象属性可修改)
❓ 3. 什么是执行上下文?包含哪些部分?
参考答案: 执行上下文是 JS 代码执行的环境容器,包含:
- 变量环境 :存储
var、函数声明 - 词法环境 :存储
let、const、块级绑定 - 可执行代码:待运行的语句
❓ 4. 为什么函数没有 return 会返回 undefined?
参考答案:
- 函数默认返回
undefined - 与作用域销毁无关,是语言设计规则
- 作用域销毁发生在执行结束后,不影响返回值
六、总结:JS 执行机制全景图
-
V8 引擎接管代码 → 启动 JIT 编译
-
编译阶段:
- 创建执行上下文
- 变量提升(
var、函数声明) let/const进入词法环境(TDZ)- 参数绑定
-
执行阶段:
- 逐行执行代码
- 调用栈管理函数上下文
- 执行完毕后上下文销毁,触发 GC
✨ 记住 :
"先编译,再执行"是 JS 的核心原则。理解变量环境与词法环境的区别,是掌握作用域、闭包、TDZ 的钥匙。
结语
JavaScript 的执行机制看似复杂,但只要抓住 "编译阶段构建环境,执行阶段操作数据" 这一主线,就能拨开迷雾。无论是解决日常 bug,还是应对大厂面试,这套知识体系都将成为你最坚实的底层支撑。
建议 :多动手写代码,结合 console.log 和断点调试,观察变量在不同阶段的状态变化。实践,才是理解的最好老师。