JavaScript 变量声明详解:var、let 与 const 的演进与实践
摘要:本文系统梳理 JavaScript 中三种变量声明方式(
var、let、const)的核心机制,深入剖析 ES5 与 ES6 在作用域、变量提升等方面的本质差异,并结合典型错误场景,提供现代 JavaScript 开发的最佳实践建议。全文约 2000 字,适合前端开发者巩固基础。
一、引言:从混乱到规范的变量声明
在早期 JavaScript(ES5 及之前),开发者只能使用 var 声明变量。然而,var 的行为常常“反直觉”——变量似乎可以在声明前被访问、作用域边界模糊、重复声明不会报错……这些问题在大型项目中极易引发难以追踪的 bug。
2015 年,ECMAScript 6(ES6/ES2015)正式发布,引入了 let 和 const,从根本上解决了 var 的缺陷,使 JavaScript 的变量管理更加严谨、可预测。理解这三者的区别,是掌握现代 JavaScript 的基石。
二、var 的工作机制与问题
1. 函数作用域(Function Scope)
var 声明的变量仅在函数内部有效,而非代码块(如 if、for)内:
function test() {
if (true) {
var x = 10;
}
console.log(x); // 10 —— 在 if 外仍可访问!
}
这种设计导致变量“泄漏”出预期的作用域,违背模块化编程原则。
2. 变量提升(Hoisting)
JavaScript 引擎在执行代码前会经历编译阶段(用于语法检查和变量/函数声明收集)。var 声明会被“提升”到其作用域顶部,但赋值留在原地:
console.log(a); // undefined(不是报错!)
var a = 1;
等价于:
var a; // 编译阶段:声明被提升
console.log(a); // 执行阶段:a 为 undefined
a = 1; // 赋值未提升
这种“可访问但值为 undefined”的行为极易造成逻辑错误,且难以调试。
3. 允许重复声明
var name = "Alice";
var name = "Bob"; // 不报错!name 变为 "Bob"
这种宽松性在协作开发中可能掩盖命名冲突。
三、ES6 的革新:let 与 const
为解决 var 的问题,ES6 引入了块级作用域(Block Scope)和两个新关键字:
1. 块级作用域({} 内有效)
{
let y = 20;
const z = 30;
}
console.log(y); // ReferenceError: y is not defined
console.log(z); // ReferenceError: z is not defined
现在,if、for、{} 等任何代码块都能形成独立作用域,变量生命周期更清晰。
2. 暂时性死区(Temporal Dead Zone, TDZ)
虽然 let/const 也会被提升,但在声明前访问会抛出错误:
console.log(PI); // ReferenceError: Cannot access 'PI' before initialization
const PI = 3.14;
从进入作用域到变量声明之间的区域称为 TDZ。这强制开发者“先声明后使用”,避免 var 的陷阱。
3. 禁止重复声明
let count = 0;
let count = 1; // SyntaxError: Identifier 'count' has already been declared
这有助于提前发现命名冲突。
4. const 的“常量”语义
- 不可重新赋值:
const MAX = 100; MAX = 200; // TypeError: Assignment to constant variable. - 但对象/数组内容可变:
const user = { name: "Tom" }; user.name = "Jerry"; // ✅ 合法 user = {}; // ❌ 非法
const 实际上是创建一个不可变的绑定,而非不可变的值。
四、关键概念对比表
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 是(初始化为 undefined) | 是(TDZ) | 是(TDZ) |
| 重复声明 | 允许 | 禁止 | 禁止 |
| 重新赋值 | 允许 | 允许 | 禁止(绑定不可变) |
| 全局属性 | 是(如 window.a) | 否 | 否 |
五、典型错误场景解析
-
ReferenceError: height is not defined
→ 在作用域外访问let/const变量。if (true) { let height = 180; } console.log(height); // 报错 -
TypeError: Assignment to constant variable.
→ 尝试给const重新赋值。const API_URL = "https://api.example.com"; API_URL = "https://new-api.com"; // 报错 -
ReferenceError: Cannot access 'PI' before initialization
→ 在 TDZ 中访问变量。console.log(PI); const PI = 3.14; // 报错
六、函数提升 vs 变量提升
除了变量,函数声明也会被提升,且整个函数体被提升:
sayHello(); // "Hello!" —— 函数可直接调用
function sayHello() {
console.log("Hello!");
}
但函数表达式(用 var/let 声明)遵循变量提升规则:
// var 形式
console.log(typeof greet); // "function"
greet(); // "Hi"
var greet = function() { console.log("Hi"); };
// let 形式
console.log(typeof hello); // ReferenceError(TDZ)
let hello = function() { console.log("Hi"); };
七、const常量可变性
- const常量声明的简单数据类型的变量不会可以改变
- const常量声明的复杂数据类型比如字面量对象可以改变
const person={
age:45;
}
age=56;
注意:使用冻结操作可以使得复杂数据类型也不可以被改变
const PI = 3.1415926;
const person={
name:"ysw",
age:25,
}
// person = 'hahahaha';
person .age = 21;
console.log(person);
// 简单数据类型的常量不可以改变;
// 复杂数据类型的常量,不能改变引用的地址,但是可以改变引用地址中的属性值
// 如果对象一定不可以变呢?
const wes = Object.freeze(person );//冻结对象
console.log(wes);
wes.age=456;
console.log(wes);
八、最佳实践建议
-
默认使用
const
除非明确需要重赋值,否则一律用const。这能防止意外修改,提高代码健壮性。 -
需要重赋值时用
let
如循环计数器、状态标志等。 -
彻底避免
var
现代项目(尤其是使用 ESLint、TypeScript)应禁用var。 -
理解 TDZ 和块级作用域
这是 LeetCode 等算法平台考察 JS 行为的关键点(如 LeetCode 热题 100 中涉及闭包、作用域的题目)。
九、结语
从 var 到 let/const,JavaScript 完成了变量管理机制的一次重要进化。这一变化不仅修复了历史遗留问题,更推动了开发者编写更安全、更可维护的代码。
记住口诀:
const优先,不变就用它;- 需要重赋值,
let来保驾;var已过时,坚决不使用。
掌握这些原理,你不仅能写出正确的代码,更能深入理解 JavaScript 的执行机制,为学习闭包、异步、模块化等高级主题打下坚实基础。