“JavaScript 的设计,是一场在时间与商业竞争夹缝中诞生的妥协。”
你有没有遇到过这样的代码:
console.log(name); // undefined
var name = "ljm";
为什么 name 是 undefined?明明我们还没给它赋值。
这背后,是 JavaScript 一个古老而深邃的设计——变量提升(Hoisting) 。
今天,我们就来深入探讨:
变量提升是如何产生的?它为何被诟病为“缺陷”?ES6 又如何用“一国两制”的方式解决这个问题?
一、变量提升:JavaScript 的“先天不足”
1.1 什么是变量提升?
在 JavaScript 中,使用 var 声明的变量会在编译阶段被“提升”到当前作用域的顶部,并初始化为 undefined。
console.log(myname); // undefined
var myname = "路明非";
这段代码等价于:
var myname; // 提升到顶部
console.log(myname); // undefined
myname = "路明非";
✅ 这就是变量提升的本质:声明提前,赋值不提前。
1.2 为什么会有变量提升?
这要回到 JavaScript 的诞生背景。
- 1995年,Brendan Eich 在 Netscape 公司仅用 10 天 设计了 JavaScript。
- 目标是:让网页动起来,不是构建复杂系统。
- 为了快速实现,设计上做了大量简化。
其中最核心的一条:不支持块级作用域。
没有块级作用域,意味着 {} 内部的变量无法隔离,于是 JS 团队决定:
把所有变量都“提升”到函数顶部,统一管理。
这样做的好处是:
- 实现简单
- 执行效率高
- 避免作用域嵌套带来的复杂性
但代价是:逻辑混乱、容易出错。
二、变量提升的“副作用”:坑多如麻
2.1 变量被意外覆盖
var name = "ljm";
function showName() {
console.log(name); // undefined
if (true) {
var name = "大厂的苗子"
}
console.log(name); // "大厂的苗子"
}
showName();
为什么第一次打印是 undefined?因为 var name 被提升到了函数顶部,导致外部的 name 被“遮蔽”。
更糟糕的是,if 块内定义的变量会影响整个函数作用域,这是违反直觉的。
2.2 块级作用域缺失
for(var i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // 3
i 没有被限制在循环内,而是泄露到了全局。这在现代开发中是灾难性的。
三、ES6 的救赎:let/const 与“一国两制”
3.1 ES6 引入块级作用域
let name = "ljm";
{
let name = "大厂的苗子";
console.log(name); // "大厂的苗子"
}
console.log(name); // "ljm"
✅ let 和 const 支持块级作用域,变量只在 {} 内有效。
但这只是表象。真正的秘密在于:执行上下文的内部结构发生了变化。
四、执行上下文:变量环境 vs 词法环境
JavaScript 的执行上下文(Execution Context)包含两个核心部分:
| 环境 | 用途 | 存储内容 |
|---|---|---|
| 变量环境(Variable Environment) | 存放 var 声明的变量 | 提前声明,初始为 undefined |
| 词法环境(Lexical Environment) | 存放 let/const 声明的变量 | 支持块级作用域,有栈结构 |
🌟 关键点:var 和 let 被分治管理!
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a); // 1
console.log(b); // 3
}
console.log(b); // 2
console.log(c); // 4
console.log(d); // ReferenceError: d is not defined
}
foo();
图解:foo 函数的执行上下文
第一步:编译阶段(创建执行上下文)
- 变量环境:
a = 1,c = undefined - 词法环境:
b = undefined(外层),b = undefined, d = undefined(内层)
⚠️ 注意:
let b = 2会被放入词法环境的外层栈帧,而let b = 3放入内层栈帧。
第二步:执行阶段
当执行到 {} 内部时:
- 词法环境压入新栈帧:
b = 3,d = 5 - 查找变量时,先从栈顶开始(最近的作用域)
- 找不到再向上查找
🔍
console.log(a)→ 查找变量环境 →a = 1
🔍console.log(b)→ 查找栈顶 →b = 3
执行完 {} 后,栈帧弹出,b=3, d=5 不再可访问。
五、“暂时性死区”:避免错误的利器
let name = "ljm";
{
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "大厂的苗子";
}
let 声明的变量在声明之前不可访问,这就是“暂时性死区(Temporal Dead Zone, TDZ) ”。
为什么需要 TDZ?
防止在变量未初始化前就使用它,比如:
let x = y + 1;
let y = 10; // ❌ 错误!y 尚未定义
TDZ 确保了变量必须在声明后才能使用,提升了代码的安全性。
六、历史的回响:为何保留 var?
虽然 let/const 更安全,但 JavaScript 必须向下兼容。
- 海量旧代码依赖
var var的行为已深入人心(尽管是坏习惯)
所以 ES6 采取了“一国两制”策略:
| 特性 | var | let/const |
|---|---|---|
| 提升 | ✅ 是 | ❌ 否(但声明提前) |
| 块级作用域 | ❌ 否 | ✅ 是 |
| 暂时性死区 | ❌ 否 | ✅ 是 |
| 重复声明 | ✅ 允许 | ❌ 禁止 |
💡 本质是:用不同的机制处理不同需求,既保留兼容性,又引入现代特性。
七、总结:作用域的演进之路
| 时代 | 作用域 | 变量提升 | 块级作用域 | 问题 |
|---|---|---|---|---|
| ES5 | 函数作用域 | ✅ 是 | ❌ 否 | 变量污染、逻辑混乱 |
| ES6+ | 函数 + 块级 | var: ✅;let: ❌ | ✅ 是 | 安全、清晰 |
🧠 核心思想:
- 变量提升是历史产物,源于早期设计的简化。
- 块级作用域是现代需求,要求更严格的变量控制。
- ES6 通过“变量环境”和“词法环境”的分离,实现了两者共存。
八、图注解析
1:编译阶段的执行上下文
var a = 1→ 变量环境:a = 1let b = 2→ 词法环境:b = undefined(声明提前)var c = 4→ 变量环境:c = undefinedlet d = 5→ 词法环境:d = undefined
✅ 此时变量尚未赋值,但已存在。
2:执行阶段的变量查找
console.log(a)→ 查找变量环境 →a = 1console.log(b)→ 查找词法环境栈顶 →b = 3
🔁 词法环境像一个栈,每次进入块级作用域就压入新帧,退出时弹出。
3:词法环境的栈结构
- 外层:
b = undefined - 内层:
b = 3,d = 5 - 执行完成后,内层帧被销毁,
d不再可访问。
九、写在最后
JavaScript 的作用域系统,是一段技术与现实博弈的历史。
从最初的“快速上线”,到如今的“严谨规范”,我们看到了语言的成熟。
🌱 好的设计,不一定完美,但一定是在不断修正中前行。
当你下次看到 let 和 var 的区别时,不妨想想:
这不只是语法差异,更是一代开发者对“安全”与“兼容”的深刻思考。