🎭 人物登场:var、let和const的江湖地位
1. var:老派程序员的“万能钥匙”
- 特点:函数作用域 + 变量提升(Hoisting)
- 缺点:容易“越界”(作用域污染)、重复声明、无TDZ陷阱
- 经典台词:
console.log(a); // undefined 😬 var a = 10;
var
就像一位“老江湖”,总喜欢提前打招呼(变量提升),但有时会让人摸不着头脑(undefined)。
2. let:新时代的“守序骑士”
- 特点:块级作用域 + 暂时性死区(TDZ)
- 优点:杜绝“意外交友”(重复声明报错)、更安全的作用域
- 经典台词:
console.log(b); // ReferenceError ❌ let b = 20;
let
像一位严谨的骑士,坚守规则(块级作用域),不允许在声明前访问变量(TDZ),否则直接“封印”你的代码!
3. const:不可变的“铁血战士”
- 特点:块级作用域 + 声明即赋值(不可变)
- 优点:防止“篡改数据”、提升代码可读性
- 经典台词:
const c = 30; c = 40; // TypeError ⚠️
const
是“铁血战士”,一旦设定值,便终身不变。如果你试图修改它?对不起,直接“阵亡”!
🧠 背后的故事:执行上下文与调用栈的“暗流涌动”
1. 变量查找的路径:从当前作用域到全局作用域
JavaScript引擎查找变量时遵循作用域链(Scope Chain),规则如下:
- 当前作用域 → 2. 父作用域 → 3. 全局作用域
- 如果变量在当前作用域找不到,会逐层向上冒泡查找,直到找到或到达全局作用域。
示例:
const globalVar = "全局变量";
function outer() {
const outerVar = "外层函数变量";
function inner() {
const innerVar = "内层函数变量";
console.log(innerVar); // ✅ 内层作用域
console.log(outerVar); // ✅ 父作用域
console.log(globalVar); // ✅ 全局作用域
}
inner();
}
outer();
变量查找就像“寻宝游戏”:先在自己家找(当前作用域),找不到就去隔壁(父作用域),最后去整个小区(全局作用域)!
2. 变量提升(Hoisting)
JavaScript引擎在编译阶段会将变量声明和函数声明“潜入”到作用域顶部,但它们的行为不同:
var
:声明被提升,赋值未完成时值为undefined
。console.log(d); // undefined var d = 5;
let/const
:声明被提升,但赋值未完成时进入暂时性死区(TDZ),访问会抛出错误。console.log(e); // ReferenceError let e = 6;
变量提升就像“快递提前送到”,但
var
的快递箱是空的(undefined
),而let/const
的快递箱被“封印”了(TDZ)。
🔍 深度解析:var、let、const的“实战演练”
1. 嵌套作用域与冒泡查找
当变量在多个作用域中存在时,JavaScript会优先使用最近的作用域中的变量:
const global = "我是全局";
function test() {
const local = "我是局部";
console.log(global); // ✅ 全局作用域
console.log(local); // ✅ 局部作用域
}
test();
嵌套作用域就像“俄罗斯套娃”,最内层的变量优先级最高!
🚨 避坑指南:var、let、const的“雷区”
1. var的“无意识提升”
console.log(d); // undefined
var d = 5;
var
的变量提升会让你误以为变量已初始化,实际上只是声明被提升了,赋值还在后面!
2. let/const的“暂时性死区(TDZ)”
什么是TDZ?
- TDZ(Temporal Dead Zone,暂时性死区) 是指变量声明之后但在初始化之前的一段时间。
- 在TDZ内访问变量,JavaScript引擎会直接报错(
ReferenceError
),而不是返回undefined
。
为什么TDZ存在?
- 为了防止开发者在变量未初始化时误操作,增强代码的安全性。
- 例如:
console.log(e); // ReferenceError ❌ let e = 6;
let/const
的声明被提升到块级作用域顶部,但赋值(初始化)未完成,此时访问变量会进入TDZ,直接报错!
TDZ的对比
变量类型 | 声明提升 | TDZ行为 | 示例 |
---|---|---|---|
var | ✅ | ❌(返回undefined ) | console.log(a); var a = 1; |
let | ✅ | ✅(报错) | console.log(b); let b = 2; |
const | ✅ | ✅(报错) | console.log(c); const c = 3; |
3. const的“铁血规则”
const obj = { name: "Alice" };
obj = { name: "Bob" }; // TypeError
obj.name = "Bob"; // ✅ 允许修改属性
const
保护的是变量的引用地址,不是值本身。如果变量是对象,属性是可以修改的!
📜 总结:变量声明的“江湖法则”(小白详解版)
在JavaScript中,var
、let
和const
是三种声明变量的方式,但它们的行为差异巨大。以下通过对比+实例的方式,详细解释它们的特性,帮助你彻底理解!
1️⃣ 作用域(Scope)
问题:变量在哪里“活着”?
-
var
:函数作用域(Function Scope)- 只有在函数内部声明的变量,才属于该函数作用域。
- 示例:
function example() { var a = 10; // 属于example函数的作用域 if (true) { var b = 20; // 同样属于example函数作用域! } console.log(b); // ✅ 输出20 } example(); console.log(a); // ❌ 报错:a未定义(全局作用域找不到a)
- 注意:
var
在函数内部声明时,即使在if
块中,也会“逃出”到整个函数作用域!
-
let
/const
:块级作用域(Block Scope)- 变量只在声明它的代码块
{}
内有效。 - 示例:
function example() { let c = 30; // 属于example函数作用域 if (true) { let d = 40; // 属于if代码块作用域 } console.log(d); // ❌ 报错:d未定义(超出if块作用域) } example();
- 好处:避免变量“越界”,减少意外覆盖。
- 变量只在声明它的代码块
2️⃣ 变量提升(Hoisting)
问题:变量声明会被“提前”吗?
-
var
:声明被提升,赋值不提升- 声明会被“搬运”到函数顶部,但赋值留在原地。
- 示例:
console.log(e); // undefined var e = 5; // 实际编译为: var e; console.log(e); // undefined e = 5;
-
let
/const
:声明被提升,但进入“暂时性死区(TDZ)”- 声明被提升到块级作用域顶部,但赋值前访问会报错。
- 示例:
console.log(f); // ❌ ReferenceError(TDZ) let f = 6; // 实际编译为: // 声明被提升,但赋值前不能访问 let f; console.log(f); // ❌ ReferenceError f = 6;
3️⃣ 重复声明(Redeclaration)
问题:能否多次声明同名变量?
-
var
:允许重复声明- 示例:
var x = 1; var x = 2; // ✅ 允许,第二次声明会覆盖第一次
- 示例:
-
let
/const
:禁止重复声明- 示例:
let y = 3; let y = 4; // ❌ 报错:Identifier 'y' has already been declared
- 示例:
4️⃣ 可变性(Mutability)
问题:变量能被重新赋值吗?
-
var
/let
:允许重新赋值- 示例:
var z = 10; z = 20; // ✅ 允许 let w = 30; w = 40; // ✅ 允许
- 示例:
-
const
:声明后不能重新赋值- 示例:
const v = 50; v = 60; // ❌ 报错:Assignment to constant variable.
- 例外:如果声明的是对象或数组,属性/元素可以修改!
const obj = { name: "Alice" }; obj.name = "Bob"; // ✅ 允许(修改属性) obj = { name: "Charlie" }; // ❌ 报错(重新赋值)
- 示例:
5️⃣ 全局对象绑定(Global Object)
问题:在浏览器中,变量会成为window
对象的属性吗?
-
var
:会- 示例:
var globalVar = "Hello"; console.log(window.globalVar); // ✅ 输出"Hello"
- 示例:
-
let
/const
:不会- 示例:
let blockScoped = "World"; console.log(window.blockScoped); // ✅ 输出undefined
- 示例:
6️⃣ 暂时性死区(TDZ)
问题:声明前能访问变量吗?
-
var
:可以,但值为undefined
- 示例:
console.log(a); // undefined var a = 10;
- 示例:
-
let
/const
:禁止访问,直接报错- 示例:
console.log(b); // ❌ ReferenceError let b = 20;
- 示例:
7️⃣ 嵌套作用域与冒泡查找
问题:变量怎么找?
- JavaScript引擎会从当前作用域开始查找,找不到就一层层向上找(父作用域→全局作用域)。
- 示例:
const global = "全局"; function outer() { const outer = "外层"; function inner() { const inner = "内层"; console.log(inner); // ✅ 内层 console.log(outer); // ✅ 外层 console.log(global); // ✅ 全局 } inner(); } outer();
🧾 总结表格
特性 | var | let | const |
---|---|---|---|
作用域 | 函数作用域(容易污染) | 块级作用域(更安全) | 块级作用域(更安全) |
变量提升 | ✅ 声明提升,值为undefined | ✅ 声明提升,但进入TDZ | ✅ 声明提升,但进入TDZ |
TDZ | ❌ 无,返回undefined | ✅ 有,报错 | ✅ 有,报错 |
重复声明 | ✅ 允许 | ❌ 禁止 | ❌ 禁止 |
可变性 | ✅ 可重新赋值 | ✅ 可重新赋值 | ❌ 不可重新赋值(值可变) |
全局绑定 | ✅ 会成为window 属性 | ❌ 不会 | ❌ 不会 |
适用场景 | 旧代码兼容 | 需要重新赋值的变量 | 常量(如配置项、固定值) |
🚀 新增内容:var与let的性能对比 🚀
特性 | var | let |
---|---|---|
作用域链查找 | 函数作用域,查找范围广 🐢 | 块级作用域,查找更高效 🚀 |
内存管理 | 容易造成内存泄漏(污染) | 严格作用域,内存更干净 ✅ |
执行速度 | 略慢(作用域链长) | 更快(作用域链短) |
实际应用建议 | 仅用于旧代码维护 | 现代开发首选 |
⚡ 性能小贴士:虽然现代JavaScript引擎优化了性能差异,但
let
的块级作用域设计在大型项目中能显著减少内存占用和作用域冲突!
🌈 现代开发的最佳实践
- 优先使用
let
和const
:避免var
的作用域污染和意外提升。 - 用
const
声明不可变值:如配置项、常量等。 - 避免
var
:除非需要兼容旧代码或特定场景。
🎁 彩蛋:用表情符号记住它们!
var
:👴(老派、容易出错)let
:🛡️(安全、灵活)const
:🧊(冰封、不可变)- 性能差异:🐢 vs 🚀(
var
慢,let
快) - 作用域污染:💣(
var
的“地雷”) vs ✅(let
的“盾牌”)
🚀 结语
通过对比var
、let
和const
的特性,你会发现:
let
和const
是现代JavaScript的首选,它们解决了var
的许多痛点,让代码更安全、更易维护!- 下次写代码时,记得用
let
和const
,远离var
的“坑”吧! 🌟
练习题:你能解释以下代码的输出吗?
for (var i = 0; i < 3; i++) {}
console.log(i); // 输出?
for (let j = 0; j < 3; j++) {}
console.log(j); // 输出?
答案揭晓:
var
的i
会泄漏到全局作用域,输出3
。let
的j
仅在for
块内有效,输出ReferenceError
!
快去试试看吧! 😄