📜写在前面:JavaScript 作为一门动态、弱类型的脚本语言,其变量声明机制经历了从 ES5(ECMAScript 5)到 ES6(ECMAScript 2015)的重大演进。本文深入剖析
var、let和const三者的区别、行为特征、作用域规则、提升机制、暂时性死区(Temporal Dead Zone, TDZ)等核心概念,并辅以代码示例和原理讲解,力求做到“详细就完了”!
💡 一、ES5 时代的变量声明:var 的“坏味道”
1.1 var 声明的基本语法
var age = 18;
这是 ES5 中唯一的变量声明方式。它具有以下特点:
- 函数作用域(Function Scope):不是块级作用域。
- 变量提升(Hoisting):在编译阶段,所有
var声明会被“提升”到当前作用域顶部。 - 可重复声明:同一个作用域内可以多次用
var声明同名变量。 - 隐式全局变量:在非严格模式下,未声明直接赋值的变量会成为全局对象的属性。
1.2 变量提升(Hoisting)详解
来看你的 2.js 中的例子:
console.log(age); // undefined
var age = 18;
这段代码在 JavaScript 引擎中实际被处理为:
var age; // 编译阶段:声明被提升
console.log(age); // 执行阶段:此时 age 是 undefined
age = 18; // 赋值发生在执行阶段
✅ 关键点:
var的声明被提升,但赋值不会提升。- 这导致在声明前访问变量不会报错,而是返回
undefined。 - 这种行为违反直觉,容易引发 bug,尤其在大型项目中难以追踪。
🚫 正如
readme.md所说:“var申明变量 bad 和直觉不符合”,且“变量提升不利于代码的可读性,应该废弃的糟粕”。
1.3 var 不支持块级作用域
看 4.js 的例子:
{
var age = 18; // 在块中声明
let height = 188;
}
console.log(age); // ✅ 输出 18
console.log(height); // ❌ ReferenceError: height is not defined
var age虽然写在{}块中,但由于var没有块级作用域,它实际上属于外层作用域(这里是全局)。- 因此
console.log(age)能正常访问。 - 而
let height具有块级作用域,块外无法访问。
🔍 结论:
var的作用域是函数级或全局级,不是块级。这在if、for、{}等结构中极易造成变量污染。
🆕 二、ES6 革命:let 与 const 的登场
ES6(2015)引入了 let 和 const,解决了 var 的诸多问题。
2.1 let:块级作用域 + 暂时性死区
✅ 特性总结:
- 块级作用域(Block Scope):
{}内部形成独立作用域。 - 不可重复声明:同一作用域内不能重复
let同名变量。 - 无变量提升(表现为有 TDZ):在声明前访问会抛出
ReferenceError。 - 存在暂时性死区(Temporal Dead Zone, TDZ)
🧪 示例(来自 2.js):
console.log(PI); // ❌ ReferenceError: Cannot access 'PI' before initialization
let height = 188;
const PI = 3.1415926;
⚠️ 注意:虽然
let/const在编译阶段也会被“识别”,但它们在初始化前处于 TDZ,任何访问都会报错。
🔬 什么是暂时性死区(TDZ)?
TDZ 是指从块开始到变量声明语句之间的区域,在此区域内访问该变量会抛出 ReferenceError。
{
console.log(x); // ❌ TDZ:Cannot access 'x' before initialization
let x = 10;
}
📚 原理:JS 引擎在进入块作用域时,会为
let/const变量预留内存位置,但标记为“未初始化”。只有执行到声明语句时才完成初始化。在此之前访问 = 报错。
这有效防止了“先使用后声明”的逻辑错误,提升了代码健壮性。
2.2 const:不可重新赋值的常量
✅ 特性总结:
- 所有
let的特性(块级作用域、TDZ、不可重复声明) - 必须初始化:声明时必须赋值
- 不能重新赋值:
const a = 1; a = 2;→TypeError - 但对象/数组内容可变(引用不变,内容可改)
🧪 示例(来自 3.js):
const PI = 3.1415926;
const person = { name: "ysw", age: 28 };
// person = 'hahaha'; // ❌ TypeError: Assignment to constant variable.
person.age = 21; // ✅ 允许!修改对象属性
console.log(person); // { name: "ysw", age: 21 }
💡 关键理解:
const保证的是绑定(binding)不可变,即变量名永远指向同一个内存地址。- 对于基本类型(number, string, boolean),值存储在栈中,地址即值 → 值不可变。
- 对于引用类型(object, array),地址指向堆中的对象 → 地址不变,但对象内部可变。
🔒 如何真正“冻结”对象?
若希望对象内容也不可变,需使用 Object.freeze():
const wes = Object.freeze(person);
wes.age = 17; // 在严格模式下静默失败,非严格模式也无效
console.log(wes); // age 仍是 21(或原值)
⚠️ 注意:
Object.freeze()是浅冻结,嵌套对象仍可变。如需深度冻结,需递归实现。
🔄 三、函数提升 vs 变量提升(来自 5.js)
3.1 函数是一等公民(First-Class Citizen)
JavaScript 中函数可以:
- 赋值给变量
- 作为参数传递
- 作为返回值
3.2 函数声明提升 vs var 提升
看 5.js 的代码:
setWidth(); // ✅ 可以调用!
function setWidth() {
var width = 100;
console.log(width);
}
这里发生了函数提升(Function Hoisting):
- 函数声明(
function foo() {})会被完整提升(包括函数体)。 - 而
var只提升声明,不提升赋值。
对比:
foo(); // ✅ 正常执行
function foo() { console.log('I am hoisted!'); }
bar(); // ❌ TypeError: bar is not a function
var bar = function() { console.log('Not hoisted as function'); };
📌 结论:
- 函数声明提升 >
var提升 - 函数表达式(赋值给变量的函数)遵循
var/let的提升规则
🧩 四、作用域体系全景图
| 声明方式 | 作用域类型 | 是否提升 | TDZ | 可重复声明 | 可重新赋值 |
|---|---|---|---|---|---|
var | 函数/全局 | ✅ 声明提升 | ❌ | ✅ | ✅ |
let | 块级 | ❌(表现为 TDZ) | ✅ | ❌ | ✅ |
const | 块级 | ❌(表现为 TDZ) | ✅ | ❌ | ❌ |
🎯 最佳实践:
- 永远不要使用
var(除非维护老旧代码) - 默认使用
const - 当需要重新赋值时,改用
let - 避免在块外访问块内变量
🧠 五、错误类型详解(来自 readme.md)
你提到的几个典型错误:
1. ReferenceError: height is not defined
- 原因:访问了未声明的变量(或块级作用域外访问
let/const) - 示例:
4.js中console.log(height)在块外调用
2. TypeError: Assignment to constant variable.
- 原因:试图对
const变量重新赋值 - 示例:
1.js中key = 'abc234'(key是const)
3. ReferenceError: Cannot access 'PI' before initialization
- 原因:在 TDZ 中访问
let/const变量 - 示例:
2.js中console.log(PI)在const PI = ...之前
📚 六、延伸思考:为什么 JS 会有“编译阶段”?
虽然 JS 是解释型语言,但它在执行前会经历一个快速的编译过程(由 V8、SpiderMonkey 等引擎完成):
- 词法分析(Lexing):将代码转为 token 流
- 语法分析(Parsing):构建 AST(抽象语法树)
- 编译(Compilation):
- 识别所有变量、函数声明
- 确定作用域链
- 处理
var提升、函数提升 - 标记
let/const的 TDZ 区域
- 执行(Execution):逐行运行代码
🌟 正因为有这个“编译阶段”,才有了“提升”和“TDZ”等行为。
✅ 七、总结:现代 JavaScript 变量声明指南
| 场景 | 推荐写法 |
|---|---|
| 声明后不再改变的值 | const |
| 循环计数器、状态变量等 | let |
| 兼容 IE 或老旧环境(不推荐) | var |
🚀 记住:
const优先:大多数变量其实不需要重新赋值。- 块级作用域是现代 JS 的基石:避免变量泄漏。
- TDZ 是保护机制:防止“先用后声明”的逻辑错误。
- 函数声明提升很强大:但也要注意可读性。
📖 推荐阅读
- 《你不知道的 JavaScript(上卷)》第 1–2 章(你已提及)
- MDN Web Docs: let, const
- LeetCode Top 100 Liked(巩固算法同时练习现代 JS 写法)
🎉 结语:从 var 的混乱到 let/const 的清晰,JavaScript 正在变得越来越像一门“严肃”的编程语言。掌握这些细节,不仅能写出更安全的代码,还能在面试中脱颖而出!继续加油,攻克吧!💪🔥