let、const与var的区别:JavaScript变量声明最佳实践

作者:青柠

时间:2026-11-05

浏览:485

深入解析 JavaScript 中 let、const 与 var 的核心机制差异,涵盖作用域模型、变量提升与暂时性死区(TDZ)、内存可变性与全局对象挂载,并提供现代工程开发中的变量声明决策树与最佳实践。

文章目录
JavaScript 进阶教程 / 语言核心

分类:前端工程开发 · 适用环境:ECMAScript 2015 (ES6) 及更高版本

let、const与var的区别:JavaScript变量声明最佳实践
图示:let、const 与 var 的核心机制特性与现代开发定位
在现代 JavaScript 开发中,变量声明的基本原则可以明确概括为一条准则:优先默认使用 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(每次迭代拥有独立的块级上下文绑定)
let、const与var的区别:JavaScript变量声明最佳实践
图 1:函数作用域与块级作用域在执行上下文中的边界划分与变量泄漏对比

三、区别二:变量提升与暂时性死区(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"
let、const与var的区别:JavaScript变量声明最佳实践
图 2:变量提升与暂时性死区(TDZ)在执行生命周期各阶段的时序流转图

四、区别三:内存绑定与可变性——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)。
let、const与var的区别:JavaScript变量声明最佳实践
图 3:const 在基本类型与引用类型下的栈堆内存结构与操作权限划分

五、区别四:全局顶层对象的属性挂载

在浏览器全局作用域(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变量声明最佳实践
图 4:现代 JavaScript 变量声明决策流与工程质量保障支柱

七、总结

let、const 与 var 的演进是 JavaScript 语言迈向工业化与工程安全性的关键里程碑。从 var 的函数作用域与松散提升,到 let 和 const 的块级作用域与暂时性死区防护,语言从底层机制上堵住了变量非预期泄露与覆盖的漏洞。

在日常开发中,只要坚守“默认 const、按需 let、杜绝 var”的极简策略,并善用工程自动化工具(如 ESLint 与 TypeScript)进行静态约束,即可从根源上保障变量声明的严谨性与代码库的长期健康。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章