为什么你的代码总出错?💣很有可能是因为没搞懂var、let、const✅

183 阅读8分钟

🎭 人物登场:var、let和const的江湖地位

1. var:老派程序员的“万能钥匙”

  • 特点:函数作用域 + 变量提升(Hoisting)
  • 缺点:容易“越界”(作用域污染)、重复声明、无TDZ陷阱
  • 经典台词
    console.log(a); // undefined 😬  
    var a = 10;  
    

    var就像一位“老江湖”,总喜欢提前打招呼(变量提升),但有时会让人摸不着头脑(undefined)。

image.png


2. let:新时代的“守序骑士”

  • 特点:块级作用域 + 暂时性死区(TDZ)
  • 优点:杜绝“意外交友”(重复声明报错)、更安全的作用域
  • 经典台词
    console.log(b); // ReferenceError ❌  
    let b = 20;  
    

    let像一位严谨的骑士,坚守规则(块级作用域),不允许在声明前访问变量(TDZ),否则直接“封印”你的代码!

image.png

3. const:不可变的“铁血战士”

  • 特点:块级作用域 + 声明即赋值(不可变)
  • 优点:防止“篡改数据”、提升代码可读性
  • 经典台词
    const c = 30;  
    c = 40; // TypeError ⚠️  
    

    const是“铁血战士”,一旦设定值,便终身不变。如果你试图修改它?对不起,直接“阵亡”!

image.png

🧠 背后的故事:执行上下文与调用栈的“暗流涌动”

1. 变量查找的路径:从当前作用域到全局作用域

JavaScript引擎查找变量时遵循作用域链(Scope Chain),规则如下:

  1. 当前作用域 → 2. 父作用域 → 3. 全局作用域
    • 如果变量在当前作用域找不到,会逐层向上冒泡查找,直到找到或到达全局作用域。

示例

const globalVar = "全局变量";  
function outer() {  
  const outerVar = "外层函数变量";  
  function inner() {  
    const innerVar = "内层函数变量";  
    console.log(innerVar); // ✅ 内层作用域  
    console.log(outerVar); // ✅ 父作用域  
    console.log(globalVar); // ✅ 全局作用域  
  }  
  inner();  
}  
outer();  

变量查找就像“寻宝游戏”:先在自己家找(当前作用域),找不到就去隔壁(父作用域),最后去整个小区(全局作用域)!

image.png

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)。

4b67c93d7993541975d91ca3daaa086.png

image.png

🔍 深度解析: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❌(返回undefinedconsole.log(a); var a = 1;
let✅(报错)console.log(b); let b = 2;
const✅(报错)console.log(c); const c = 3;

image.png

3. const的“铁血规则”

const obj = { name: "Alice" };  
obj = { name: "Bob" }; // TypeError  
obj.name = "Bob"; // ✅ 允许修改属性  

const保护的是变量的引用地址,不是值本身。如果变量是对象,属性是可以修改的!


📜 总结:变量声明的“江湖法则”(小白详解版)

在JavaScript中,varletconst是三种声明变量的方式,但它们的行为差异巨大。以下通过对比+实例的方式,详细解释它们的特性,帮助你彻底理解!


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();  
    

🧾 总结表格

特性varletconst
作用域函数作用域(容易污染)块级作用域(更安全)块级作用域(更安全)
变量提升✅ 声明提升,值为undefined✅ 声明提升,但进入TDZ✅ 声明提升,但进入TDZ
TDZ❌ 无,返回undefined✅ 有,报错✅ 有,报错
重复声明✅ 允许❌ 禁止❌ 禁止
可变性✅ 可重新赋值✅ 可重新赋值❌ 不可重新赋值(值可变)
全局绑定✅ 会成为window属性❌ 不会❌ 不会
适用场景旧代码兼容需要重新赋值的变量常量(如配置项、固定值)

🚀 新增内容:var与let的性能对比 🚀

特性varlet
作用域链查找函数作用域,查找范围广 🐢块级作用域,查找更高效 🚀
内存管理容易造成内存泄漏(污染)严格作用域,内存更干净 ✅
执行速度略慢(作用域链长)更快(作用域链短)
实际应用建议仅用于旧代码维护现代开发首选

性能小贴士:虽然现代JavaScript引擎优化了性能差异,但let的块级作用域设计在大型项目中能显著减少内存占用和作用域冲突!


🌈 现代开发的最佳实践

  • 优先使用letconst:避免var的作用域污染和意外提升。
  • const声明不可变值:如配置项、常量等。
  • 避免var:除非需要兼容旧代码或特定场景。

🎁 彩蛋:用表情符号记住它们!

  • var:👴(老派、容易出错)
  • let:🛡️(安全、灵活)
  • const:🧊(冰封、不可变)
  • 性能差异:🐢 vs 🚀(var慢,let快)
  • 作用域污染:💣(var的“地雷”) vs ✅(let的“盾牌”)

🚀 结语

通过对比varletconst的特性,你会发现:

  • letconst是现代JavaScript的首选,它们解决了var的许多痛点,让代码更安全、更易维护!
  • 下次写代码时,记得用letconst,远离var的“坑”吧! 🌟

55bb1f2bcc9580de602923ccf38a1de.png

练习题:你能解释以下代码的输出吗?

for (var i = 0; i < 3; i++) {}  
console.log(i); // 输出?  

for (let j = 0; j < 3; j++) {}  
console.log(j); // 输出?  

答案揭晓:

  • vari会泄漏到全局作用域,输出3
  • letj仅在for块内有效,输出ReferenceError

快去试试看吧! 😄