在 JavaScript 的世界里,var、let 和 const 是每个开发者每天都会用到的三个关键字。初学者常被教导:“用 const 声明不能变的,let 声明能变的,别用 var。”
但如果你以为它们的区别仅仅是“能不能重新赋值”,那你可能错过了 JavaScript 引擎背后更精妙的设计哲学。
今天,让我们撕开表面,深入作用域、提升机制、内存模型和工程实践,看看这三个关键字究竟藏着怎样的秘密。
一、你以为的“不变”,其实很脆弱
很多人说:const 声明的是“常量”,不能修改。
但请看这段代码:
const user = { name: 'Alice' };
user.name = 'Bob'; // ✅ 成功!
console.log(user); // { name: 'Bob' }
咦?不是说 const 不能改吗?怎么对象属性还能变?
真相是: const 并不保证“值不变”,而是保证“绑定不变”(immutable binding)。 它冻结的是变量与内存地址之间的引用关系,而不是内存中的数据本身。
- 对于基本类型(string, number, boolean 等):值直接存储在变量中,所以确实“不可变”。
- 对于引用类型(object, array 等):变量存的是指向堆内存的指针,
const只禁止你把指针指向别处,但不阻止你修改指针所指的内容。
若真想让对象“完全不可变”,你需要:
const user = Object.freeze({ name: 'Alice' });
user.name = 'Bob'; // 在严格模式下会报错(非严格模式静默失败)
💡 关键认知:
const≠ 不可变数据,而是不可重绑定(reassignment)。
二、作用域:从“函数级”到“块级”的革命
var:函数作用域 + 变量提升(Hoisting)
function example() {
console.log(a); // undefined(不是报错!)
var a = 10;
}
var 会被“提升”到函数顶部,等价于:
function example() {
var a; // 声明被提升
console.log(a); // undefined
a = 10; // 赋值留在原地
}
更可怕的是,在 if 或 for 中声明的 var,会泄露到整个函数:
if (true) {
var x = 1;
}
console.log(x); // 1 —— 意外暴露!
let / const:真正的块级作用域(Block Scope)
if (true) {
let y = 2;
const z = 3;
}
console.log(y); // ReferenceError: y is not defined
console.log(z); // ReferenceError: z is not defined
它们被限制在 {} 所定义的块内,彻底解决了“变量污染”问题。
🌟 历史意义:ES6 引入 let/const,标志着 JavaScript 正式拥抱现代编程语言的作用域模型。
三、暂时性死区(Temporal Dead Zone, TDZ):let/const 的“安全锁”
这是 let 和 const 最容易被忽视却极其重要的特性。
console.log(a); // ReferenceError!
let a = 5;
对比 var 的 undefined,这里直接报错!为什么?
因为在代码执行到 let a = 5 之前,a 处于 TDZ(暂时性死区) —— 虽然 JS 引擎知道这个变量存在(词法分析阶段已注册),但不允许访问,直到初始化完成。
TDZ 的价值:
-
防止“先使用后声明”的逻辑错误
-
强制开发者写出更清晰、顺序合理的代码
-
避免因提升导致的隐蔽 bug ⚠️ 注意:typeof 在 TDZ 中也不安全!
typeof undeclared; // "undefined"(正常) typeof tdzVar; // ReferenceError! let tdzVar;
四、循环中的经典陷阱:var vs let
看这个面试高频题:
// 使用 var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3 3 3
原因:所有回调共享同一个 i 变量,循环结束时 i === 3。
而用 let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0 1 2
魔法在哪?
每次循环迭代,let 都会创建一个新的块级作用域,并把当前 i 的值“捕获”进去。这本质上是一个隐式的闭包!
🔍 技术细节:JS 引擎为每次迭代生成一个新的 LexicalEnvironment,i 被绑定到该环境。
五、工程视角:为什么我们几乎不再用 var?
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数级 | 块级 | 块级 |
| 提升 | 是(初始化为 undefined) | 是(但有 TDZ) | 是(但有 TDZ) |
| 重复声明 | 允许(静默覆盖) | 不允许 | 不允许 |
| 全局污染 | 会挂载到 window/global | 不会 | 不会 |
| 可预测性 | 低 | 高 | 最高 |
在现代工程化项目中:
const是默认选择:除非你知道变量需要重新赋值,否则一律用const。let是例外情况:用于计数器、状态切换等明确需要变更的场景。var已成历史:除非维护老旧代码,否则应彻底避免。
📜 社区共识:ESLint 规则
no-var已成为标配。
六、终极建议:如何写出更安全的代码?
- 默认用
const让“不可变”成为常态,迫使你思考是否真的需要变更。 - 需要重赋值时才用
let并确保作用域尽可能小(如 for 循环内部)。 - 彻底告别
var它的设计缺陷在复杂应用中会引发难以追踪的 bug。 - 理解“不变性”的层次
const→ 绑定不变Object.freeze()→ 浅层数据不变- Immutable.js / Immer → 深层不可变数据结构
结语:语法糖之下,是语言演进的智慧
var、let、const 的演进,不只是语法的更新,更是 JavaScript 从“脚本玩具”走向“工程语言”的缩影。
它们背后是作用域模型的革新、内存管理的优化,以及对开发者心智负担的减轻。
下次当你写下 const 时,请记住:你不仅是在声明一个变量,更是在参与一场持续十年的语言进化。
真正的高手,不是知道“怎么写”,而是明白“为什么这样设计”。