JavaScript中var、let和const声明变量的区别与代码示例

作者:闭包旅人时间:2026-11-05 18:57:43浏览:349

深入对比JavaScript中var、let与const变量声明关键字的核心区别,涵盖函数作用域与块级作用域、变量提升与暂时性死区(TDZ)、标识符重新声明、内存引用可变性以及工程实践中的变量选型规范与代码重构示例。

JavaScript中var、let和const声明变量的区别与代码示例
图 0:JavaScript 变量声明体系架构图(var / let / const)
语言核心机制 阅读时长:约 10 分钟 标准规范:ECMAScript 2015 (ES6) 及后续规范
在 JavaScript 编写过程中,var、let 与 const 构成了变量与常量声明的基石。其核心差异集中在作用域边界(函数作用域还是块级作用域)、变量提升与暂时性死区(TDZ)、重复声明限制以及内存绑定的可变性四个维度。在现代工程实践中,推荐遵循“默认使用 const,仅在需要重新赋值时使用 let,全面淘汰 var”的基本原则,以消除变量越界污染与提升带来的逻辑隐患。

一、核心特性全景对比

在 ECMAScript 6(ES2015)发布之前,JavaScript 仅支持通过 var 关键字声明变量。随着语言标准对工程严谨性要求的提升,引入了拥有块级作用域的 let 和 const。为了在全局层面对比这三者的语言行为,下表归纳了它们在语法约束和底层运行时中的关键特征:

对比维度 var let const
作用域类型 函数作用域(Function Scope) 块级作用域(Block Scope) 块级作用域(Block Scope)
变量提升 (Hoisting) 提升声明并自动初始化为 undefined 提升声明,但不初始化(存在暂时性死区 TDZ) 提升声明,但不初始化(存在暂时性死区 TDZ)
声明时必须赋初值 否 否 是(若未赋初值报语法错误)
是否支持重新赋值 支持 支持 不支持(锁定栈内存引用绑定)
同一作用域重复声明 允许(后声明覆盖前者) 禁止(抛出 SyntaxError) 禁止(抛出 SyntaxError)
全局作用域属性挂载 挂载到 window / globalThis 不挂载,独立保存在全局词法环境记录中 不挂载,独立保存在全局词法环境记录中

二、作用域机制:函数作用域与块级作用域

作用域决定了变量在源码中的可访问区域与生命周期。var 遵循函数作用域规则,这意味着只要变量在函数内部声明,无论嵌套在多少层 if、for、while 或花括号代码块内部,外部整个函数均可直接读写该变量。

相反,let 和 const 遵循块级作用域(由一对花括号 { ... } 界定)。任何在块内声明的标识符,在块执行结束之后其对应的词法环境(Lexical Environment)即刻销毁,外部上下文无法进行越界访问。

JavaScript中var、let和const声明变量的区别与代码示例
图 1:var 作用域穿透与 let/const 块级词法环境隔离机制对比

1. 代码块作用域隔离对比示例

// 示例 1:var 穿透 if 代码块
function varScopeDemo() {
  if (true) {
    var message = "var 穿透了代码块";
  }
  // 正常输出:var 不受 if 代码块限制
  console.log(message); // 输出: "var 穿透了代码块"
}
varScopeDemo();

// 示例 2:let 与 const 受限于块级作用域
function blockScopeDemo() {
  if (true) {
    let blockLet = "局限于块内";
    const blockConst = "同样局限于块内";
  }
  // 尝试越界访问
  try {
    console.log(blockLet);
  } catch (err) {
    console.log(err.name); // 输出: "ReferenceError"
  }
}
blockScopeDemo();

2. 经典循环与异步回调中的闭包问题

在 for 循环中绑定异步回调(如 setTimeout 或 DOM 点击事件)时,var 与 let 表现出根本性差异。由于 var 在整个循环中共享同一个函数级变量 i,异步任务执行时循环早已结束,读取到的均为最终累加值;而 let 在每次循环迭代时都会创建一个全新的块级词法环境并记录当前快照:

// 问题场景:使用 var 声明循环变量
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log("var 计数器:", i); // 连续三次输出: "var 计数器: 3"
  }, 100);
}

// 修复方案:使用 let 声明循环变量
for (let j = 0; j < 3; j++) {
  setTimeout(() => {
    console.log("let 计数器:", j); // 依次输出: "let 计数器: 0", "let 计数器: 1", "let 计数器: 2"
  }, 100);
}
原理细节说明
在 ES5 时代,为了修复 var 导致的异步循环陷阱,开发者必须手动编写立即执行函数表达式(IIFE)来构建闭包保存当前状态。使用 let 后,引擎规范在规范层面为每次迭代绑定了独立的块级环境记录,无需冗余嵌套即可实现状态隔离。

三、变量提升机制与暂时性死区(TDZ)

在 JavaScript 代码执行前,V8 等引擎会先进行词法分析与预编译。在此阶段,所有的变量声明都会被引擎感知并进行内存槽位分配,这一现象常被称为变量提升(Hoisting)。

然而,“声明提升”并不等于“可用性提升”。深入 ECMAScript 规范,变量生命周期可解构为三个连续阶段:

  1. 创建阶段(Creation):在执行上下文的作用域中注册标识符名称,分配作用域槽位。
  2. 初始化阶段(Initialization):将标识符与初始值绑定。若未显式赋值,默认赋予 undefined。
  3. 赋值阶段(Assignment):运行代码中的赋值表达式,将业务数据存入变量。

var 在进入作用域时,创建与初始化阶段同时完成(直接赋予 undefined),因此即使在声明代码之前读取也不会抛错;而 let 与 const 仅在进入作用域时完成了创建阶段,初始化阶段必须等到实际执行流到达声明语句那一行。从进入作用域到声明语句执行完成之间的这片代码区间,被称为暂时性死区(Temporal Dead Zone, TDZ)。在 TDZ 内存取该变量会直接抛出 ReferenceError。

JavaScript中var、let和const声明变量的区别与代码示例
图 2:变量生命周期阶段推导与暂时性死区(TDZ)状态迁移流程

代码提升与 TDZ 行为对比示例

// 1. var 变量提升的表现
console.log(elevatedVar); // 输出: undefined(已创建并初始化,但未赋值)
var elevatedVar = "赋值完成";
console.log(elevatedVar); // 输出: "赋值完成"

// 2. let 在声明前处于暂时性死区 (TDZ)
try {
  console.log(deadZoneLet); // 抛出 ReferenceError: Cannot access 'deadZoneLet' before initialization
} catch (e) {
  console.log("捕获异常:", e.message);
}
let deadZoneLet = "走出 TDZ";

// 3. TDZ 的“时间性”特征:与代码物理行数无关,只与执行时间有关
function temporalProof() {
  const invoke = () => console.log(timingVar); // 在物理位置上在声明之前定义
  let timingVar = "此时已完成初始化";
  invoke(); // 运行成功,输出: "此时已完成初始化"
}
temporalProof();

四、重复声明与 const 的内存绑定可变性

除了作用域与生命周期,var 与 let/const 在语法安全检查上有着截然不同的严格程度。

1. 重复声明校验

在同一词法作用域内,var 允许对同一标识符进行多次声明,后续声明仅被视为普通的赋值或无操作,这极易掩盖大型项目中由于命名冲突导致的变量覆盖 Bug。而 let 和 const 只要检测到同名标识符存在,即刻在编译阶段报出 SyntaxError: Identifier has already been declared。

var user = "Alice";
var user = "Bob"; // 允许重复声明,user 被意外覆盖

let count = 1;
// let count = 2; // 语法报错: SyntaxError: Identifier 'count' has already been declared

2. const 的“不可变”是引用绑定不可变,而非实体不可变

很多初学者常误认为使用 const 声明的对象或数组也是“只读常量”。实际上,const 锁定的仅仅是栈内存中保存的变量绑定(即指针地址),而非堆内存中实际存放的数据对象。

JavaScript中var、let和const声明变量的区别与代码示例
图 3:const 变量在栈内存与堆内存中的物理绑定模型
// 基础类型(数值、字符串、布尔):值直接保存在栈中,不可改变
const MAX_LIMIT = 500;
// MAX_LIMIT = 600; // 抛出 TypeError: Assignment to constant variable.

// 引用类型(对象、数组):栈中保存的是内存地址
const databaseConfig = {
  host: "localhost",
  port: 3306
};

// 允许修改或追加堆内部的属性
databaseConfig.port = 3307;
databaseConfig.user = "admin";
console.log(databaseConfig.port); // 输出: 3307

// 若试图重新为变量绑定新对象地址,则会报错
try {
  databaseConfig = { host: "remote" }; // 改变栈指针
} catch (err) {
  console.log(err.name); // 输出: "TypeError"
}

// 如果需要真正冻结对象内容,需配合 Object.freeze()
const immutableSystem = Object.freeze({
  version: "1.0.0"
});
// 在严格模式下 ('use strict'),修改属性会抛出 TypeError;非严格模式下静默忽略
// immutableSystem.version = "2.0.0";

3. 全局对象污染防范

在浏览器全局环境中(未启用模块化脚本时),使用 var 声明的全局变量会自动成为全局顶层对象 window 的属性(例如 var a = 1; window.a === 1)。这不仅容易意外覆盖 window.name 等浏览器原有原生 API,还会造成全局命名空间的严重污染。

使用 let 和 const 在最外层声明变量时,变量会被存放在 ECMAScript 规范所定义的全局词法环境记录(Global Lexical Environment Record)中,绝不会挂载到 window 或 globalThis 上,保障了宿主环境全局对象的纯净性。

五、工程实践:选型决策树与旧代码安全重构指南

规范的变量声明选型不仅能够提升代码可读性,更能协助 JavaScript 引擎执行更深入的 JIT 优化(如内联缓存与隐藏类优化)。在编写新业务或重构旧项目时,可严格参考下述决策流程与排查步骤。

JavaScript中var、let和const声明变量的区别与代码示例
图 4:现代 JavaScript 变量声明选型决策树与历史代码安全重构操作流程

1. 日常编码选型准则

  • 默认声明优先使用 const:绝大多数业务中的数据(函数定义、配置字典、模块导入实例、DOM 节点引用)在生命周期内均不应且不需要更换内存引用地址。默认使用 const 可以明确向协作团队传达“该标识符绑定恒定不变”的语义意图。
  • 仅在明确需要修改引用或重新赋值时使用 let:例如在 for 循环计数器、累计求和变量、状态机状态切换标志位或条件分支内晚期初始化的变量。
  • 禁止在新项目中使用 var:现代前端工具链(ESLint、TypeScript、Babel)通常会将 no-var 列为推荐或强制校验规则。

2. 历史项目中 var 变量的安全重构 4 步法

  1. 排查块外部逃逸访问:先检查历史代码中是否利用了 var 的作用域穿透特性(如在 if 代码块内部声明,却在 if 代码块外部使用)。若存在此类情况,需将变量声明提前至公共外层。
  2. 初步全局批量替换为 const:使用编辑器或重构工具先将作用域内的 var 统一替换为 const。
  3. 借助编译器 / Lint 工具识别重赋值并降级为 let:运行构建或代码静态分析,若发现有 TypeError: Assignment to constant variable,说明该变量存在重新赋值行为,精确将这部分声明调整为 let。
  4. 检查并优化循环与异步闭包:核对所有在循环体中声明的 var,改为 let 后即可安全移除旧代码中为了避免变量共享而封装的冗长闭包(IIFE)。

六、常见认知误区与避坑汇总

⚠️ 误区一:let 和 const 变量根本没有提升?
纠正:let 与 const 同样经历变量提升!不同之处在于它们的提升仅停留在“创建(Creation)”阶段,并未进行“初始化(Initialization)”。如果完全没有提升,代码在遇到未初始化的标识符时应该直接向外层作用域查找,但实际情况是当前作用域内的 let 声明直接“遮蔽”了外层同名变量并触发死区报错,这正是提升存在的最直接技术证明。
⚠️ 误区二:const 声明的数组使用 push 会报错?
纠正:push、pop、splice 等方法仅仅是修改了堆内存中数组连续存储单元的数据,并没有改变该数组在栈内存中对应的首地址指针。因此 const arr = []; arr.push(1); 是完全合法的语法行为。只有当执行 arr = [1, 2] 时才会抛出异常。

综上所述,var、let 和 const 见证了 JavaScript 从一门弱约束的脚本语言演进为现代化大型工程语言的发展历程。深入理解底层内存绑定、作用域生命周期与 TDZ 原理,能够让开发者在编写日常代码时从源头上杜绝隐式作用域泄漏、提升脏读与并发异步状态污染等经典缺陷。

作者最新文章
相关文章
更多