深入详解JavaScript中var、let与const在作用域规则、生命周期机制、变量提升与暂时性死区(TDZ)等方面的核心差异,结合代码示例与原理解析提供清晰的声明选型指南。
JavaScript变量声明详解:var、let与const的作用域区别
时间:2026-11-03
浏览:456
文章目录
var 到 ES6 引入 let 与 const 的重构。三者的核心本质区别体现在:作用域层级(函数作用域 vs 块级作用域)、提升行为与生命周期(挂载初始值 undefined vs 暂时性死区 TDZ 拦截)、重复声明约束以及标识符的可重赋值性。现代前端开发推行“优先 const,需要重赋值时用 let,彻底停用 var”的标准准则。
图 0:var、let 与 const 核心机制与特性全景对比
一、作用域机制的核心分水岭:函数作用域与块级作用域
在理解变量声明前,必须先厘清 JavaScript 中的作用域定义。所谓作用域(Scope),即变量与函数的可访问有效范围,它决定了执行上下文对标识符的查找权限。
在 ECMAScript 5 及之前的版本中,JavaScript 仅存在两种作用域:全局作用域(Global Scope)与函数作用域(Function Scope)。使用 var 声明的变量,其可见性完全由其所在的最近函数包裹体决定;无论该变量写在 if 分支、for 循环还是裸的花括号 {} 代码块中,变量都会直接“击穿”花括号,渗透到外部的整个函数体内。
function testVarScope() {
if (true) {
var message = "外部可见";
}
// var 穿透了 if 的代码块,在整个函数内均可正常访问
console.log(message); // 输出: "外部可见"
}
为了解决这种变量逃逸引发的数据污染与命名冲突,ES6 正式引入了块级作用域(Block Scope),并配套提供了 let 与 const 关键字。任何使用花括号包裹的语句块(如 if (...) {}、for (...) {}、while (...) {} 甚至独立的 { ... }),都会形成一个独立的词法环境。在块内通过 let 或 const 声明的变量,其生命周期严格限制在当前块内,块外访问会直接抛出引用错误 ReferenceError。
function testBlockScope() {
if (true) {
let blockVal = 100;
const blockConst = 200;
}
// 外部作用域无法穿透访问块内变量
console.log(blockVal); // 抛出: Uncaught ReferenceError: blockVal is not defined
console.log(blockConst); // 抛出: Uncaught ReferenceError: blockConst is not defined
}
图 1:代码块界限穿透与块级隔离对照
二、声明周期与执行机制:变量提升(Hoisting)与暂时性死区(TDZ)
JavaScript 脚本在被 V8 或其他 JavaScript 引擎执行时,并非简单的逐行解释运行,而是分为编译/创建阶段(Creation Phase)与执行阶段(Execution Phase)。变量在内存中的生命周期通常包含三个步骤:
- 创建(Creation / Declaration):在作用域对应的环境记录中注册该变量名。
- 初始化(Initialization):为变量在内存中分配空间并赋予初始值。
- 赋值(Assignment):将运行时具体的业务数据写入变量。
1. var 的提升机制(Hoisting)
对于使用 var 声明的变量,引擎在编译阶段会将创建与初始化同时提升至当前作用域的顶部,并自动赋予初始值 undefined。这意味着在代码实际执行到赋值语句之前,该变量就已经存在且可以被安全读取,返回 undefined,绝不抛出错误:
console.log(item); // 输出: undefined(已创建并初始化,但未执行赋值)
var item = "Apple";
console.log(item); // 输出: "Apple"
2. let 与 const 的暂时性死区(TDZ - Temporal Dead Zone)
let 和 const 同样会被引擎感知并在进入块级作用域时完成“创建”,但引擎不会自动进行初始化。从进入该块级作用域开始,直到代码执行流完整经过变量声明语句的那一行,这段时间区间被称为暂时性死区(Temporal Dead Zone,简称 TDZ)。
在 TDZ 期间,任何试图读取或写入该标识符的操作都会触发 ReferenceError: Cannot access '...' before initialization。值得注意的是,传统在 JavaScript 中被视为绝对安全的 typeof 运算符在遇到处于 TDZ 的变量时,同样会直接抛出引用异常:
// 进入作用域,x 已被创建,但处于暂时性死区 (TDZ)
console.log(typeof x); // 抛出: ReferenceError: Cannot access 'x' before initialization
let x = 42; // TDZ 终结点:初始化完成并完成赋值
console.log(typeof x); // 输出: "number"
图 2:生命周期推导——变量提升与暂时性死区(TDZ)运行阶段
三、循环结构中的作用域陷阱:for 循环与异步闭包
作用域差异在控制流与异步交互场景中表现得尤为明显。前端开发中最经典的面试与排错问题之一,便是 for 循环配合异步回调(如定时器或事件监听)时的变量捕获行为。
1. var 的单实例全局共享陷阱
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// 输出结果:
// 3
// 3
// 3
在这个代码片段中,由于 var i 不具备块级作用域特性,它直接依附在外部函数(或全局环境)中。整个循环执行期间,内存中只存在一个 i 变量。当 100 毫秒后定时器触发回调函数时,循环早已经结束,此时共享的 i 最终累加值已被更新为 3,因此三个回调函数输出的均是同一个内存地址中的数值 3。
2. let 的每次迭代独立词法绑定(Lexical Binding)
当将声明关键字改为 let 时,行为发生了根本性的转变:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
// 输出结果:
// 0
// 1
// 2
按照 ECMAScript 规范,在 for (let i = 0; ...) 语句中,JavaScript 引擎会在每一次循环迭代时,都为循环体创建一个崭新的块级词法环境(Lexical Environment),并将上一次迭代结束时的 i 值作为当前环境的初始值克隆进来。因此,每个定时器内部闭包捕获的都是属于其对应单次迭代的独立 i 实例,彼此完全隔离。
图 3:for 循环中异步回调对变量的捕获与闭包模型
四、声明约束与边界特性:重复声明、全局挂载与可变性
除了作用域与生命周期,var、let 与 const 在日常语法使用上还存在三个关键的边界约束规则:
1. 重复声明(Redeclaration)约束
var 容许在同一个作用域中重复声明同名变量,后声明的语句仅相当于赋值操作,极易掩盖变量被意外覆盖的逻辑 Bug:
var user = "Alice";
var user = "Bob"; // 允许执行,完全无告警,静默覆盖前值
而 let 与 const 具备严格的静态语法检查,在同一作用域(不管是全局、函数还是代码块)内若出现重复声明同名标识符,编译器会直接在解析阶段抛出语法错误 SyntaxError: Identifier '...' has already been declared,彻底杜绝命名冲突。
2. 全局对象属性挂载(Window Binding)
在浏览器全局执行环境中:
- 使用
var声明的全局变量,会自动挂载为全局对象(window)的属性,例如var title = "App";使得window.title === "App"。这种机制容易意外篡改浏览器内置原生属性(例如window.name)。 - 使用
let或const声明的全局变量,虽然在全局任何地方都可访问,但它们存储在底层的全局词法环境(Global Lexical Environment)中,绝不会挂载至window对象,即window.title会返回undefined,保持顶层对象的纯净。
3. const 的不可变性边界(Immutability vs Object Mutation)
许多初学者容易对 const 产生“声明了常量便完全不可修改”的误解。实际上,const 约束的仅仅是变量标识符与内存地址之间的绑定关系(Binding),即引用地址不可变更,而非其指向的具体数据内容不可变:
const user = { name: "Tom" };
user.name = "Jerry"; // 完全合法:对象内部属性可以修改
user.age = 25; // 完全合法:可以添加属性
// 抛出 TypeError: Assignment to constant variable.
user = { name: "Jerry" }; // 试图重新绑定引用地址,立即报错
Object.freeze(obj) 方法进行浅冻结,或使用递归函数实现深度冻结。
五、声明选型指南与决策总结
综合以上各维度的对比分析,三者的特性总结如下表所示:
| 特性对比项 | var | let | const |
|---|---|---|---|
| 作用域级别 | 函数作用域(Function) | 块级作用域(Block) | 块级作用域(Block) |
| 变量提升与初始化 | 提升并初始化为 undefined |
创建提升,但未初始化(进入 TDZ) | 创建提升,但未初始化(进入 TDZ) |
| 暂时性死区 (TDZ) | 不存在(声明前读值为 undefined) | 存在(提前访问抛 ReferenceError) | 存在(提前访问抛 ReferenceError) |
| 必须赋初始值 | 否(默认 undefined) | 否(默认 undefined) | 是(缺省抛 SyntaxError) |
| 重复声明限制 | 允许(同名直接覆盖) | 严格禁止(抛 SyntaxError) | 严格禁止(抛 SyntaxError) |
| 是否支持重新赋值 | 支持 | 支持 | 禁止(抛 TypeError) |
| 全局顶层对象挂载 | 挂载到 window / global |
不挂载 | 不挂载 |
图 4:变量声明最佳实践决策流程树
现代开发的核心法则
- 默认全面使用
const:让不可变性作为程序设计的缺省行为。只有在声明之初就明确该变量需要在后续逻辑中被重新赋值时,才退而求其次选择let。这有助于降低代码复杂度,并在重构时防止意外的状态污染。 - 彻底弃用
var:var附带的函数穿透、静默重复声明与未初始化提升是无数早期经典缺陷的温床。现代工程代码规范(如 ESLint 的no-var、prefer-const)已将其列为必须遵循的自动化检查标准。 - 最小作用域原则:始终在最接近使用位置的最小
{}块中声明变量,配合块级作用域能够让垃圾回收机制(GC)更及时地释放无用内存。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。