深入解析 JavaScript 中 let、const 与 var 的核心机制差异,涵盖作用域模型、变量提升与暂时性死区(TDZ)、内存可变性与全局对象挂载,并提供现代工程开发中的变量声明决策树与最佳实践。
let、const与var的区别:JavaScript变量声明最佳实践
时间:2026-11-05
浏览:485
文章目录
分类:前端工程开发 · 适用环境:ECMAScript 2015 (ES6) 及更高版本
const,仅在需要重新赋值时使用 let,彻底停用 var。理解这三者的深层差异,不仅关乎语法选择,更直接影响代码执行的安全性、内存可预测性以及团队协作中的维护成本。本文围绕作用域模型、变量提升与暂时性死区、引用可变性与全局对象绑定四个关键维度展开剖析,并提供生产环境中的声明决策流与避坑指南。
一、核心机制速查表
为了在实际编码与代码评审中快速做判断,下表系统梳理了 var、let 和 const 在 ECMAScript 规范定义下的核心行为差异:
| 比较维度 | var (ES5 及更早) | let (ES6+) | const (ES6+) |
|---|---|---|---|
| 作用域级别 | 函数作用域(Function Scope) | 块级作用域(Block Scope) | 块级作用域(Block Scope) |
| 变量提升 (Hoisting) | 提升声明并自动初始化为 undefined |
提升声明,未初始化(进入 TDZ) | 提升声明,未初始化(进入 TDZ) |
| 暂时性死区 (TDZ) | 无死区,声明前读取返回 undefined |
有死区,声明前访问抛出 ReferenceError |
有死区,声明前访问抛出 ReferenceError |
| 同作用域重复声明 | 允许(后声明者覆盖前声明者) | 禁止(抛出 SyntaxError) |
禁止(抛出 SyntaxError) |
| 重新赋值 (Re-assignment) | 允许 | 允许 | 禁止(栈内存引用锁定,抛出 TypeError) |
| 声明时必须初始化 | 否(默认 undefined) |
否(默认 undefined) |
是(必须立即赋初值) |
| 全局顶层对象绑定 | 绑定至全局对象属性(如 window.a) |
不绑定,仅存于声明式环境记录中 | 不绑定,仅存于声明式环境记录中 |
二、区别一:作用域机制——函数作用域 vs 块级作用域
var 遵循函数作用域规则,这意味着变量仅在声明它的函数内部受限;如果它声明在普通的流程控制代码块(如 if、for、while 或单纯的独立大括号 { })中,该变量会直接穿透代码块边界,泄漏到外层包含函数或全局作用域中。
与此相对,let 和 const 具备严格的块级作用域约束。任何由一对大括号包围的区域都会成为它们合法的物理隔离边界,一旦执行流程跳出该大括号,其内部声明的变量即刻离开词法环境并被回收。
// 示例 1:if 代码块中的作用域泄漏对比
function testScope() {
if (true) {
var varVariable = "var 能逃出代码块";
let letVariable = "let 被锁定在代码块中";
}
console.log(varVariable); // 正常输出: "var 能逃出代码块"
// console.log(letVariable); // 抛出异常: ReferenceError: letVariable is not defined
}
testScope();
这种作用域泄漏在经典循环中尤为显著。使用 var 声明循环索引时,所有异步回调共享的是同一个外层变量;而 let 会在每次循环迭代时创建全新的词法环境与独立绑定:
// 示例 2:for 循环闭包中的迭代变量行为
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var 索引:", i), 10);
}
// 输出示例: 3, 3, 3(共享外部同一个 i,执行时已递增至 3)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let 索引:", j), 10);
}
// 输出示例: 0, 1, 2(每次迭代拥有独立的块级上下文绑定)
三、区别二:变量提升与暂时性死区(TDZ)
JavaScript 引擎在执行一段代码前,会先经历编译阶段(创建执行上下文与环境记录),再进入执行阶段逐行求值。提升(Hoisting)正是发生在这两个阶段交替时的行为体现。
对于 var,引擎在编译期为其在环境记录中开辟存储空间,并立刻赋予初始值 undefined。因此在执行期,即便在 var a = 1; 这行代码前面访问 a,也不会崩溃,而是读取到预先初始化的 undefined。
对于 let 与 const,许多人误以为它们“不存在变量提升”,这是不符合规范的误解。实际上,引擎同样会在编译期识别并注册它们的名字,但明确禁止将其初始化。从作用域开始创建,直到代码执行流推进到声明语句的那一瞬间,这片时间/代码窗口被称为暂时性死区(Temporal Dead Zone,简称 TDZ)。
// 示例 3:暂时性死区(TDZ)的具体行为表现
console.log(hoistedVar); // 正常输出: undefined(提升并已初始化)
var hoistedVar = 10;
// 处于 TDZ 期间:
// 任何对该标识符的读取(包括看似无害的 typeof)都会直接抛错
// console.log(typeof deadZoneLet); // 抛出异常: ReferenceError: Cannot access 'deadZoneLet' before initialization
let deadZoneLet = 20; // TDZ 在此处结束并完成初始化绑定
console.log(deadZoneLet); // 正常输出: 20
暂时性死区并不是空间位置上的绝对概念,而是一个时间维度上的时序概念。只要函数调用发生在变量声明初始化之后,即便函数物理书写在变量上方,也可以正常访问:
// 示例 4:时序性死区验证
function printValue() {
console.log(target); // 正常访问:因为函数被调用时,target 已完成初始化
}
let target = "ready";
printValue(); // 输出: "ready"
四、区别三:内存绑定与可变性——const 的真实边界
const 声明创建的是对某个值的只读引用(Read-Only Reference)。很多初学者容易产生一种误解,认为“用 const 声明的对象或数组是不允许被修改的”。要看清真相,必须区分 JavaScript 运行时的两种内存区域:栈内存(Stack)与堆内存(Heap)。
- 基本数据类型(Number、String、Boolean、Symbol、BigInt、null、undefined):其数值直接保存在栈内存中。
const锁定了栈中的具体数据,因此一旦尝试重新赋值(如count = 2),就会直接引发TypeError: Assignment to constant variable。 - 引用数据类型(Object、Array、Function 等):栈内存中保存的仅仅是一个指向堆内存真实对象的内存地址指针(Pointer)。
const保证的是这个指针地址不可变,即不能让它指向新的对象;但堆内存中该对象内部的键值、数组成员,本身并没有被锁定。
// 示例 5:const 对象的引用不可变 vs 属性可变
const user = {
id: 1001,
name: "林柯"
};
// ✔ 合法操作:修改或新增内部属性(栈中的指针未发生改变)
user.name = "林柯 (已验证)";
user.role = "Editor";
console.log(user); // 输出: { id: 1001, name: "林柯 (已验证)", role: "Editor" }
// 🚫 非法操作:尝试重新分配引用
// user = { id: 1002, name: "张华" }; // 抛出异常: TypeError: Assignment to constant variable
若业务场景需要确保对象内部属性同样不可变更,必须借助
Object.freeze() 进行浅冻结,或在复杂嵌套场景中结合深度冻结函数与不可变数据库(如 Immutable.js / Immer)。
五、区别四:全局顶层对象的属性挂载
在浏览器全局作用域(Global Scope)中声明变量时,var 与 let/const 存在极为重要的底层宿主差异:
- 使用
var在最外层声明的变量,会自动成为全局对象(浏览器中的window,或 Node.js 环境中的global)的属性,即var appTitle = "CMS"会创建window.appTitle。 - 使用
let或const在最外层声明的变量,属于全局执行上下文的“声明式环境记录”(Declarative Environment Record)。它们不会挂载到window上,从而杜绝了污染全局对象和意外覆盖原生属性(如window.name、window.location)的系统级隐患。
// 示例 6:全局属性挂载检测(浏览器环境运行)
var globalVar = "挂载到 window";
let globalLet = "不污染 window";
console.log(window.globalVar); // 正常输出: "挂载到 window"
console.log(window.globalLet); // 输出: undefined
console.log(globalLet); // 正常输出: "不污染 window"
六、现代工程中的变量声明决策树与最佳实践
在企业级团队协作和工程化代码库中,遵循清晰的变量声明规范能够显著降低调试成本,增强程序可读性。结合现代 JavaScript 规范,建议在实际开发中严格贯彻以下四条核心实践法则:
1. 实践一:优先默认使用 const
编写任何函数或模块时,凡是声明新标识符,起手第一反应应当是使用 const。这能向阅读代码的队友和未来的自己传递一个明确意图:“这个引用在当前上下文中是稳定的,不会被意外覆盖”。它消除了大量由于变量复用、意外赋值而产生的隐蔽逻辑漏洞,并且使代码更容易推理和进行静态分析。
2. 实践二:仅在必须重新绑定时使用 let
只有当某个变量的值必然随着程序流转而发生重新赋值时,才将其退化为 let。典型场景包括:
for循环中的计数器游标(如for (let i = 0; ...))。- 在多个条件分支语句(
if...else)中分别赋初值的临时接收变量。 - 累加器、动态标志位(如
let isCompleted = false;)。
3. 实践三:在现代项目中全面淘汰 var
var 是 ES5 时代遗留的历史产物,在现代开发中没有任何不可替代的优势,反而附带了变量提升掩盖错误、缺少块级作用域以及全局污染等多重负面副作用。在项目根目录的 .eslintrc.json 中应当无条件启用规则:
{
"rules": {
"no-var": "error",
"prefer-const": "error"
}
}
4. 实践四:遵循“就近声明与作用域最小化”原则
避免在函数顶部一次性声明大量未使用的变量。变量的声明位置应该尽可能紧贴其首次使用的逻辑片段。在 switch 语句中,如果需要在 case 内使用 let 或 const 声明变量,务必用显式的大括号 { } 将该 case 包裹为独立块级作用域,否则多个 case 共享同一词法环境会导致标识符重复声明语法错误:
// 示例 7:switch 语句中正确隔离块级作用域
switch (actionType) {
case "ADD": {
// 显式使用大括号建立块级边界,防止 payload 与其他分支命名冲突
const payload = { id: 1, text: "新建项" };
processItem(payload);
break;
}
case "DELETE": {
const payload = { id: 1 }; // ✔ 安全:各自处于独立块级作用域中
removeItem(payload);
break;
}
}
七、总结
let、const 与 var 的演进是 JavaScript 语言迈向工业化与工程安全性的关键里程碑。从 var 的函数作用域与松散提升,到 let 和 const 的块级作用域与暂时性死区防护,语言从底层机制上堵住了变量非预期泄露与覆盖的漏洞。
在日常开发中,只要坚守“默认 const、按需 let、杜绝 var”的极简策略,并善用工程自动化工具(如 ESLint 与 TypeScript)进行静态约束,即可从根源上保障变量声明的严谨性与代码库的长期健康。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。