var、let、const 的区别不只是“能不能改”——深入 JavaScript 变量声明的本质

4 阅读5分钟

在 JavaScript 的世界里,varletconst 是每个开发者每天都会用到的三个关键字。初学者常被教导:“用 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;         // 赋值留在原地
}

更可怕的是,在 iffor 中声明的 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 的“安全锁”

这是 letconst 最容易被忽视却极其重要的特性。

console.log(a); // ReferenceError!
let a = 5;

对比 varundefined,这里直接报错!为什么?

因为在代码执行到 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?

特性varletconst
作用域函数级块级块级
提升是(初始化为 undefined)是(但有 TDZ)是(但有 TDZ)
重复声明允许(静默覆盖)不允许不允许
全局污染会挂载到 window/global不会不会
可预测性最高

在现代工程化项目中:

  • const 是默认选择:除非你知道变量需要重新赋值,否则一律用 const
  • let 是例外情况:用于计数器、状态切换等明确需要变更的场景。
  • var 已成历史:除非维护老旧代码,否则应彻底避免。

📜 社区共识:ESLint 规则 no-var 已成为标配。

六、终极建议:如何写出更安全的代码?

  1. 默认用 const 让“不可变”成为常态,迫使你思考是否真的需要变更。
  2. 需要重赋值时才用 let 并确保作用域尽可能小(如 for 循环内部)。
  3. 彻底告别 var 它的设计缺陷在复杂应用中会引发难以追踪的 bug。
  4. 理解“不变性”的层次
    • const → 绑定不变
    • Object.freeze() → 浅层数据不变
    • Immutable.js / Immer → 深层不可变数据结构

结语:语法糖之下,是语言演进的智慧

varletconst 的演进,不只是语法的更新,更是 JavaScript 从“脚本玩具”走向“工程语言”的缩影。 它们背后是作用域模型的革新、内存管理的优化,以及对开发者心智负担的减轻。

下次当你写下 const 时,请记住:你不仅是在声明一个变量,更是在参与一场持续十年的语言进化。

真正的高手,不是知道“怎么写”,而是明白“为什么这样设计”。