人人都会AI编程

8.1 变量声明:var、let、const 的区别与底层差异

更新时间:2026-07-11

在 JavaScript 中,声明变量的方式有三种:varletconst。虽然表面上看都是在“创建一个变量”,但它们在作用域、提升行为、重复声明、与全局对象的关系等方面存在本质差异——这些差异直接来源于执行上下文和词法环境的底层设计。

8.1.1 var 的特性与缺陷

var 是 ES5 及之前唯一的变量声明方式,它带有一些历史遗留的“反直觉”行为。

函数作用域

var 声明的变量遵循函数作用域(function scope),而不是块级作用域。也就是说,在 iffor{ ... } 这类用花括号包围的代码块中声明的 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 之后逐渐被 letconst 替代。

8.1.2 let 与 const 的现代特性

letconst 是 ES6 引入的新声明方式,它们解决了 var 的大多数痛点。两者基本特性一致,唯一的差别是:let 声明的变量可以重新赋值,const 声明的变量必须在声明时赋值,且不能重新赋值(即常量的引用不可变)。

块级作用域

letconst 遵循块级作用域(block scope),也就是它们的作用范围被限制在最近的 { ... } 内,包括 ifforwhile、单独的花括号等。

{
  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)

letconst 声明的变量也会提升(hoist),但与 var 不同,它们不会被初始化为 undefined。从进入作用域到声明语句执行之前,变量处于“暂时性死区”(Temporal Dead Zone, TDZ),任何对其的访问都会导致 ReferenceError

console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 1;

TDZ 的存在强迫开发者声明变量后再使用,避免了因变量提升而导致的隐蔽 bug。

禁止重复声明

在同一作用域内,letconst 不允许重复声明同名变量,否则会直接抛出语法错误。

let a = 1;
let a = 2; // SyntaxError: Identifier 'a' has already been declared

不与全局对象绑定

在全局作用域下用 letconst 声明的变量,不会成为 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 底层差异:词法环境中的存储

从执行上下文的角度看,varlet/const 的差异源于它们存放在不同的词法环境中。

  • 在执行上下文中,存在一个变量环境(Variable Environment)组件和一个词法环境(Lexical Environment)组件。
  • var 声明的变量会被放入变量环境,而 letconst 声明的变量会放入词法环境
  • 变量环境中的变量在环境创建时就被初始化为 undefined,所以 var 有提升并可提前访问。词法环境中的变量只经历了创建,但直到声明语句执行前都处于未初始化状态,因此访问会触发 TDZ 错误。

这种底层划分,完美解释了行为上的所有差异。正是这种“先创建,后初始化”的两步走,相对于 var 的“直接初始化为 undefined”,让 letconst 更安全、更符合直觉。

8.1.5 选择建议与最佳实践

在实际开发中,大多数项目都通过 ESLint 规则禁用了 var。推荐的优先级是:

  1. 默认使用 const:只要这个变量的值不需要重新赋值,就用 const。它向代码阅读者传递了明确的意图——“这个引用不会被改变”。
  2. 确实需要重新赋值时,用 let:比如循环计数器、需要累计更新的值等。
  3. 永远避免使用 var:除非你在维护老项目或编写无法使用 ES6+ 的极端环境代码。

遵循这一简单原则,就能从根源上消除大量由变量作用域和提升导致的奇怪 bug,这也是从“能运行”走向“写好代码”的第一步。