在 JavaScript 中,声明变量的方式有三种:var、let 和 const。虽然表面上看都是在“创建一个变量”,但它们在作用域、提升行为、重复声明、与全局对象的关系等方面存在本质差异——这些差异直接来源于执行上下文和词法环境的底层设计。
8.1.1 var 的特性与缺陷
var 是 ES5 及之前唯一的变量声明方式,它带有一些历史遗留的“反直觉”行为。
函数作用域
var 声明的变量遵循函数作用域(function scope),而不是块级作用域。也就是说,在 if、for、{ ... } 这类用花括号包围的代码块中声明的 var 变量,会穿透到外层函数或全局中。
function test() {
if (true) {
var x = 10;
}
console.log(x); // 10,块没有限制住 var
}
test();
变量提升
在执行上下文的“创建阶段”,用 var 声明的变量会被提升(hoisting)到其所在函数或全局作用域的顶部,并被初始化为 undefined。这意味着你可以在声明语句之前访问该变量,值会是 undefined,而不是报错。
console.log(a); // undefined
var a = 5;
引擎实际执行类似于:
var a = undefined;
console.log(a); // undefined
a = 5;
重复声明
在同一作用域内,var 允许重复声明同名变量,后一个声明会被忽略(但赋值仍会覆盖)。
var b = 1;
var b = 2;
console.log(b); // 2
全局对象属性
在全局作用域下用 var 声明的变量,会成为全局对象(浏览器中是 window,Node.js 中是 global)的属性。这种特性容易造成命名污染和不经意的全局变量冲突。
var c = 100;
console.log(window.c); // 100
由于这些不太符合现代编程直觉的设计,var 在 ES6 之后逐渐被 let 和 const 替代。
8.1.2 let 与 const 的现代特性
let 和 const 是 ES6 引入的新声明方式,它们解决了 var 的大多数痛点。两者基本特性一致,唯一的差别是:let 声明的变量可以重新赋值,const 声明的变量必须在声明时赋值,且不能重新赋值(即常量的引用不可变)。
块级作用域
let 和 const 遵循块级作用域(block scope),也就是它们的作用范围被限制在最近的 { ... } 内,包括 if、for、while、单独的花括号等。
{
let x = 1;
const y = 2;
}
console.log(typeof x); // undefined,外部无法访问
for 循环中 let 的每次迭代都会创建一个独立的作用域,完美解决了经典闭包问题。
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j)); // 0, 1, 2
}
暂时性死区(TDZ)
用 let 和 const 声明的变量也会提升(hoist),但与 var 不同,它们不会被初始化为 undefined。从进入作用域到声明语句执行之前,变量处于“暂时性死区”(Temporal Dead Zone, TDZ),任何对其的访问都会导致 ReferenceError。
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 1;
TDZ 的存在强迫开发者声明变量后再使用,避免了因变量提升而导致的隐蔽 bug。
禁止重复声明
在同一作用域内,let 和 const 不允许重复声明同名变量,否则会直接抛出语法错误。
let a = 1;
let a = 2; // SyntaxError: Identifier 'a' has already been declared
不与全局对象绑定
在全局作用域下用 let 或 const 声明的变量,不会成为 window / global 的属性,与全局对象保持距离,减少了污染。
let d = 999;
console.log(window.d); // undefined
8.1.3 const 的特殊性
const 除了拥有 let 的所有特性外,额外要求声明时立即初始化,并且不能再次赋值。
const PI = 3.14;
PI = 3.14159; // TypeError: Assignment to constant variable.
需要特别注意:“不能重新赋值”针对的是变量的绑定,而不是值本身。如果 const 声明的变量指向一个对象或数组,那么修改对象的属性或数组的元素是完全允许的。
const obj = { name: "Alice" };
obj.name = "Bob"; // 没问题
obj.age = 25; // 也没问题
obj = {}; // TypeError: Assignment to constant variable.
const arr = [1, 2, 3];
arr.push(4); // 没问题
arr = [5, 6]; // TypeError
因此,const 并不能保证数据的不可变性,它只保证变量名和内存地址的绑定不变。若需要真正的不可变对象,应使用 Object.freeze() 或其他不可变数据结构。
8.1.4 底层差异:词法环境中的存储
从执行上下文的角度看,var 和 let/const 的差异源于它们存放在不同的词法环境中。
- 在执行上下文中,存在一个变量环境(Variable Environment)组件和一个词法环境(Lexical Environment)组件。
- 用
var声明的变量会被放入变量环境,而let和const声明的变量会放入词法环境。 - 变量环境中的变量在环境创建时就被初始化为
undefined,所以var有提升并可提前访问。词法环境中的变量只经历了创建,但直到声明语句执行前都处于未初始化状态,因此访问会触发 TDZ 错误。
这种底层划分,完美解释了行为上的所有差异。正是这种“先创建,后初始化”的两步走,相对于 var 的“直接初始化为 undefined”,让 let 和 const 更安全、更符合直觉。
8.1.5 选择建议与最佳实践
在实际开发中,大多数项目都通过 ESLint 规则禁用了 var。推荐的优先级是:
- 默认使用
const:只要这个变量的值不需要重新赋值,就用const。它向代码阅读者传递了明确的意图——“这个引用不会被改变”。 - 确实需要重新赋值时,用
let:比如循环计数器、需要累计更新的值等。 - 永远避免使用
var:除非你在维护老项目或编写无法使用 ES6+ 的极端环境代码。
遵循这一简单原则,就能从根源上消除大量由变量作用域和提升导致的奇怪 bug,这也是从“能运行”走向“写好代码”的第一步。