🌟 JavaScript变量三剑客:var、let、const的前世今生与内存探秘
🎩 序幕:一个让CPU怀疑人生的魔法秀
先看这段"违反常识"的代码:
magicShow() // 奥术飞弹 提前召唤魔法
console.log(wizard) //undefinde 读取未出生的巫师
var wizard = "甘道夫"
function magicShow(){
console.log('奥术飞弹!')
}
控制台会发射怎样的咒语?为什么函数能穿越时空?让我们打开JavaScript的时光机一探究竟!
🧠 Part 1:V8引擎的双面人生——编译与执行的量子纠缠
1.1 代码的奇幻旅程
graph TD
A[硬盘代码] --> B(内存加载)
B --> C[编译阶段]
C --> D{创建执行上下文}
D --> E[变量提升]
D --> F[作用域链生成]
C --> G[可执行字节码]
G --> H[执行阶段]
1.2 编译阶段的秘密日记
- 变量登记处:var声明的变量获得"预存通行证"(值=undefined)
- 函数VIP室:function声明直接入驻完整套房
- 作用域结界:创建三层防御体系(全局/函数/块级)
举个🌰:
var spell = "火球术"
function cast(){ /*...*/ }
编译后变身:
// 编译阶段
var spell = undefined
function cast = <完整的函数定义>
// 执行阶段
spell = "火球术"
🕵️♂️ Part 2:作用域迷宫——变量捉迷藏的三维地图
2.1 作用域三界
| 领域 | 结界范围 | 典型居民 | 特殊能力 |
|---|---|---|---|
| 全局领域 | 整个程序 | var变量 | 污染window |
| 函数秘境 | function内部 | 局部变量 | 保护隐私 |
| 块级次元 | {}内部(ES6+) | let/const变量 | 时空隔离 |
2.2 作用域链的盗梦空间
let globalGem = "无限宝石"
function thanos() {
let gauntlet = "无限手套"
{
let stones = ["力量","时间","空间","灵魂","现实","心灵"]
console.log(stones) // 当前维度
console.log(gauntlet) // 父维度
console.log(globalGem) // 顶层维度
}
}
搜索算法:当前维度 → 父维度 → 祖父维度 → ... → 全局维度(里面可以找到外面,但是外面找不到里面的,找不到就报错!)
🧨 Part 3:var的七宗罪——被时代抛弃的糟粕
3.1 恶魔的契约
console.log(demon) // undefined(恶魔已登记)
var demon = "路西法"
真实执行顺序:
var demon = undefined // 魔鬼的预注册
console.log(demon) // 空头支票
demon = "路西法" // 正式签约
3.2 末日审判清单
- 重复声明:同一变量可反复登记 → 冲突之源
- 无块级保护:循环变量泄露成全局恶魔
- 污染神域:自动成为window对象的子民
- 提升幻觉:代码顺序与执行结果割裂,可读性低
✨ Part 4:ES6曙光——let/const的救赎之路
4.1 TDZ:变量的量子牢笼
在 JavaScript 中,TDZ 是暂时性死区(temporal dead zone )的简称 。当程序的控制流程进入新的作用域(如 module、function 或 block 作用域 )进行实例化时,用
let或const声明的变量会先在作用域中被创建,但此时还未进行词法绑定,不能被访问,从变量创建到可访问之间的这段时间和空间就称为暂时性死区。
// 🚨 进入时空禁闭区
console.log(hero) // ReferenceError!
// ------- TDZ 开始-------
let hero = "超人"
// ------- TDZ 结束-------
原理图解:
编译阶段:登记hero(未初始化)
执行阶段:
│
├─ 访问hero → 检测到未初始化 → 抛出错误
└─ 初始化hero → 解除TDZ
4.2 块级作用域:代码的次元切割术
块级作用域是 ES6 引入的概念,通过
let和const实现。它的核心特点是: 变量绑定到代码块:{}内声明的变量仅在该块内可见。 暂时性死区(TDZ) :变量在声明前无法访问,避免变量提升带来的问题。
- 循环中的闭包陷阱(使用
var时)
先看使用 var 的经典问题:
for(var i=0; i<3; i++){
setTimeout(() => console.log(i), 1000); // 3, 3, 3
}
原因解析:
var声明的i属于函数作用域(这里是全局作用域),整个循环中只有一个i变量。- 当
setTimeout的回调函数执行时,循环早已结束,此时i的值已经变为3。- 所有回调函数共享同一个
i的引用,因此打印结果都是3。
let的魔法:块级作用域 + 循环迭代器的特殊处理
再看使用 let 的代码:
for(let i=0; i<3; i++){
setTimeout(() => console.log(i)) // 0,1,2
}
正确输出的原因:
- 块级作用域:
let声明的i绑定到每次循环的代码块,每次迭代都会创建一个新的i。- 循环迭代器的特殊处理:JavaScript 引擎在每次循环时会记住上一次迭代的值,并为下一次迭代创建新的
i。每个i都有独立的生命周期,与其他迭代互不干扰。- 闭包捕获:每个
setTimeout回调捕获的是当前迭代的i值,而不是共享同一个引用。
3.等价代码:手动模拟 let 的行为
为了更好理解,可将 let 循环展开为类似的 ES5 代码:
javascript
解释
// ES6 代码
for(let i=0; i<3; i++){
setTimeout(() => console.log(i));
}
// 等价的 ES5 模拟(使用 IIFE 创建闭包)
for(var i=0; i<3; i++){
(function(j){
setTimeout(() => console.log(j), 1000);
})(i);
}
这里的 IIFE(立即执行函数表达式)为每次迭代创建了独立的闭包,捕获了当前的 i 值(通过参数 j)。
魔法解析:每次循环创建独立副本,告别var的时空重叠!
4.3 const的冰火两重天
| 数据类型 | 可变性 | 示例 | 内存示意图 |
|---|---|---|---|
| 基本类型 | ❌ 冻结 | const PI=3.14 | 栈中值直接锁定 |
| 引用类型 | ✅ 可修改内容 | const arr=[1,2] | 栈地址锁定,堆内容可变 |
对象修改演示:
const merlin = { age: 300 }
merlin.age = 301 // ✅ 允许(修改堆数据)
merlin = { age: 302 } // 🚨 报错(修改栈地址)
🕰️ Part 5:历史长河——从玩具语言到工业重器
5.1 时代年表
timeline
1995 : JavaScript诞生(原名Mocha)
1997 : ES1标准化
2009 : ES5发布(严格模式)
2015 : ES6革命(现代JS元年)
2020 : ES2020(可选链操作符)
5.2 ES6的三大神迹
- 块级结界:支持复杂编程范式
- TDZ机制:终结变量提升乱象
- const常量:增强代码可预测性
- 模块化:告别全局污染
经典对比:
// ES5的痛
var counters = []
for(var i=0; i<3; i++){
counters.push(() => console.log(i))
}
counters[0]() // 3 😱
// ES6的优雅
let counters = []
for(let j=0; j<3; j++){
counters.push(() => console.log(j))
}
counters[0]() // 0 🎉
💡 终极内存揭秘:栈与堆的魔法世界
内存的双子星
| 栈(Stack) | 堆(Heap) | |
|---|---|---|
| 存储内容 | 基本类型+引用地址 | 对象、数组等复杂结构 |
| 访问速度 | ⚡ 闪电级(直接访问) | 🐢 较慢(间接访问) |
| 空间管理 | 自动分配/释放 | 手动GC(垃圾回收) |
| 结构特点 | 整齐如书架 | 杂乱如仓库 |
const的存储真相:
const id = 1001 // 栈中直接存储数值
const user = { id:1001 } // 栈存储指针,堆存储对象
🏻 现代JS开发者的圣典
- const优先原则:默认使用const,需要重新赋值再改let
- var流放条例:禁止在新项目中使用var
- 函数表达式圣约:
// 👍 推荐方式 const summon = () => { console.log('元素召唤!') } // 🚫 旧时代遗物 function summon() { /*...*/ } - 对象冻结术:
const elderScroll = Object.freeze({ title: "上古卷轴", power: "无限" })
🤔 灵魂试炼场
- 以下代码输出什么?
let potion = "隐身药水" { console.log(potion) let potion = "治疗药水" } - const声明对象时,如何防止属性被修改?
- 请解释:
let到底有没有变量提升?
在评论区留下你的魔法解析! ✨