🌟 JavaScript变量三剑客:var、let、const的前世今生与内存探秘

140 阅读6分钟

🌟 JavaScript变量三剑客:var、let、const的前世今生与内存探秘

🎩 序幕:一个让CPU怀疑人生的魔法秀

image.png

先看这段"违反常识"的代码:

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 末日审判清单

  1. 重复声明:同一变量可反复登记 → 冲突之源
  2. 无块级保护:循环变量泄露成全局恶魔
  3. 污染神域:自动成为window对象的子民
  4. 提升幻觉:代码顺序与执行结果割裂,可读性低

✨ 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) :变量在声明前无法访问,避免变量提升带来的问题。

  1. 循环中的闭包陷阱(使用 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
  1. 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的三大神迹

  1. 块级结界:支持复杂编程范式
  2. TDZ机制:终结变量提升乱象
  3. const常量:增强代码可预测性
  4. 模块化:告别全局污染

经典对比

// 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开发者的圣典

  1. const优先原则:默认使用const,需要重新赋值再改let
  2. var流放条例:禁止在新项目中使用var
  3. 函数表达式圣约
    // 👍 推荐方式
    const summon = () => {
        console.log('元素召唤!')
    }
    
    // 🚫 旧时代遗物
    function summon() { /*...*/ }
    
  4. 对象冻结术
    const elderScroll = Object.freeze({
        title: "上古卷轴",
        power: "无限"
    })
    

image.png

🤔 灵魂试炼场

  1. 以下代码输出什么?
    let potion = "隐身药水"
    {
        console.log(potion)
        let potion = "治疗药水"
    }
    
  2. const声明对象时,如何防止属性被修改?
  3. 请解释:let到底有没有变量提升?

在评论区留下你的魔法解析!