前言
JavaScript 的执行机制是理解其行为的核心。我们常看到
var 变量“提前声明”、let/const 报错“无法访问”,这些现象背后,是 V8 引擎对 执行上下文(Execution Context) 、变量环境(Variable Environment) 与 词法环境(Lexical Environment) 的精妙设计。
本文将结合你提供的代码示例与图示,彻底拆解:
- 为什么
var会“提升”而let/const不行? - 什么是“暂时性死区”(TDZ)?
- 如何用“一国两制”统一
var与let/const? - ES6 是如何通过词法环境实现块级作用域的?
无论你是准备大厂面试,还是想深入理解 JS 运行原理,这篇文章都将为你打开一扇通往底层世界的大门。
一、JavaScript 执行机制概览
1. V8 引擎与执行流程
JavaScript 由浏览器中的 V8 引擎 解析执行,其过程分为两个阶段:
| 阶段 | 内容 |
|---|---|
| 编译阶段 | 解析代码、构建抽象语法树(AST)、生成字节码 |
| 执行阶段 | 创建执行上下文、调用栈管理、变量查找 |
✅ 关键点:JS 是“先编译后执行”的语言,不是纯解释型。
2. 调用栈(Call Stack)
- 每次函数调用,都会创建一个 执行上下文 并压入调用栈
- 函数执行完毕,上下文出栈并回收资源
- 实现了函数嵌套调用的顺序控制
js
编辑
function a() {
b();
}
function b() {
console.log('hello');
}
a(); // 调用栈:[全局, a(), b()]
二、执行上下文:变量的“家”
每个函数或全局代码块在执行时,都会创建一个 执行上下文(Execution Context) ,它包含三个核心部分:
| 组件 | 作用 |
|---|---|
| 变量环境(Variable Environment) | 存储 var 声明的变量,支持“变量提升” |
| 词法环境(Lexical Environment) | 存储 let、const、函数声明等,支持“块级作用域” |
| this 绑定 | 确定当前作用域的 this 指向 |
📌 本质:执行上下文是变量的“容器”,决定了变量的可见性与生命周期。
三、变量提升(Hoisting):var 的“特权”
1. 什么是变量提升?
var 声明的变量在编译阶段被“提升”到作用域顶部,并初始化为 undefined。
js
编辑
console.log(a); // undefined
var a = 1;
实际执行流程:
js
编辑
var a; // 提升并初始化为 undefined
console.log(a); // 输出 undefined
a = 1; // 赋值
2. var 的问题:不科学的设计缺陷
- 容易覆盖:同一作用域内多次声明不会报错
- 无块级作用域:
if、for中的var仍影响外部 - 生命周期混乱:本该销毁的变量未被清理
js
编辑
for (var i = 0; i < 3; i++) {}
console.log(i); // 3 → 外部可访问!
💡 历史原因:
JS 初期为快速推出而设计,受浏览器竞争驱动,追求简单而非严谨。当时目标只是“给网页加动态效果”,复杂特性如类、继承、作用域都未充分考虑。
四、ES6 的救赎:let / const 与“一国两制”
1. “一国两制”:两种环境并存
| 类型 | 存放位置 | 行为 |
|---|---|---|
var | 变量环境 | 提升 + 初始化为 undefined |
let / const | 词法环境 | 提升但不初始化 → 暂时性死区(TDZ) |
✅ 设计哲学:
var保持兼容,延续旧有行为let/const引入新规则,提升代码安全性
五、暂时性死区(Temporal Dead Zone, TDZ)
1. 什么是 TDZ?
在 let / const 声明之前访问变量,会抛出 ReferenceError,因为变量虽被“声明”,但尚未“初始化”。
js
编辑
let name = '刘锦苗';
{
console.log(name); // ❌ ReferenceError!进入 TDZ
let name = "大厂的苗子";
}
2. 为什么会出现 TDZ?
let声明被提升到词法环境,但初始值为 “未定义”状态- 在赋值前,任何读取操作都视为“非法访问”
- 目的是防止意外使用未初始化的变量
🔍 注意:
console.log(name)访问的是块内的name,不是外层的,因为存在“词法遮蔽”(Lexical Shadowing)。
六、词法环境:块级作用域的实现机制
1. 词法环境的结构:小型栈
词法环境内部维护了一个栈式结构,每进入一个块级作用域,就压入一个新的“环境记录”。
js
编辑
function foo() {
var a = 1;
var b = 2;
{
let b = 3; // 新环境记录
var c = 4;
let d = 5;
console.log(b); // 3(栈顶)
}
console.log(b); // 2(回到上一层)
}
2. 图解:foo 函数的执行上下文
👉 执行前(编译阶段)
plaintext
编辑
[全局]
├── 变量环境
│ ├── a = undefined
│ └── c = undefined
└── 词法环境
└── b = undefined
👉 执行中(进入块)
plaintext
编辑
[foo 函数的执行上下文]
├── 变量环境
│ ├── a = 1
│ └── c = undefined
└── 词法环境
├── [块级环境1]
│ ├── b = undefined
│ └── d = undefined
└── [块级环境2]
└── b = 2
✅ 图示说明:
- 红色区域:变量环境(存放
var)- 橙色区域:词法环境(存放
let/const)- 栈结构:块级作用域执行时,优先查找栈顶环境
七、经典案例分析
示例1:var vs let 的差异
js
编辑
// var 版本
var name = "刘锦苗";
function showName() {
console.log(name); // undefined
if (true) {
var name = "大厂的苗子";
}
console.log(name); // "大厂的苗子"
}
showName();
// let 版本
let name = "刘锦苗";
function showName() {
console.log(name); // "刘锦苗"
if (false) {
let name = "大厂的苗子"; // 不执行,不影响
}
console.log(name); // "刘锦苗"
}
showName();
示例2:for 循环中的 let 与 var
js
编辑
// var:循环变量共享
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
} // 输出 3,3,3
// let:每次迭代创建新绑定
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
} // 输出 0,1,2
💡 原因:
let在每次循环中创建新的块级作用域,i是独立的。
八、大厂面试题精选
let会提升吗?
→ 会提升声明,但不会提升初始化,进入 TDZ。- 为什么
let在声明前访问会报错?
→ 因为处于“暂时性死区”,变量未初始化。 - ES6 如何实现块级作用域?
→ 通过词法环境的栈结构,在每个块内创建独立的环境记录。 var和let的存储位置有何不同?
→var在变量环境,let在词法环境。
结语
JavaScript 的执行机制看似复杂,实则是一套精心设计的“双轨制”系统:
var保留兼容性,走“变量环境”老路let/const引入现代规范,走“词法环境”新道
这正是 ES6 的伟大之处——既尊重历史,又拥抱未来。理解这一机制,不仅能避免常见陷阱,更能写出更安全、更清晰的代码。
记住:
“变量提升”不是 bug,而是设计;
“暂时性死区”不是限制,而是保护。
真正的高手,懂得在规则中寻找自由。