1. var 的变量提升(Hoisting)

定义

  • 变量提升var 声明的变量在代码执行前会被提升到当前作用域的顶部,但 赋值操作留在原地
  • 本质是 JavaScript 引擎的预编译阶段对变量声明的处理。

代码示例

console.log(a); // 输出 undefined(变量提升但未赋值)
var a = 10;
console.log(a); // 输出 10

等价于:

var a;          // 声明提升到作用域顶部
console.log(a); // undefined
a = 10;         // 赋值留在原地
console.log(a); // 10

特点

  • 变量在声明前可访问(值为 undefined)。
  • 函数作用域生效(无块级作用域)。

注意事项

  • 函数声明优先级高于变量声明
    console.log(foo); // 输出函数体(函数声明提升优先级高)
    var foo = 20;
    function foo() {}

2. let/const 的暂时性死区(TDZ)

定义

  • 暂时性死区(TDZ):在代码块内,使用 let/const 声明的变量在声明前不可访问,否则抛出 ReferenceError
  • 目的是强制开发者先声明后使用,避免隐式错误。

代码示例

console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 20;
 
if (true) {
  console.log(c); // ❌ ReferenceError(TDZ 开始)
  const c = 30;   // TDZ 结束
}

原理

  • 变量在声明前处于 TDZ,引擎会检测此阶段的非法访问。
  • let/const 声明不会提升到作用域顶部,但会绑定到块级作用域。

注意事项

  • TDZ 与作用域链
    let x = 1;
    {
      console.log(x); // ❌ ReferenceError(当前块级作用域的 x 在 TDZ)
      let x = 2;
    }

3. 块级作用域实现

定义

  • 块级作用域:由 {} 包裹的代码块(如 if/for)形成独立作用域,let/const 声明的变量仅在此块内有效。
  • ES6 通过 let/const 引入块级作用域,解决 var 的变量泄露问题。

代码示例

// var 的变量泄露
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i)); // 输出 3, 3, 3
}
 
// let 的块级作用域
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j)); // 输出 0, 1, 2
}

实现原理

  • 每次循环迭代生成一个新的块级作用域,let 声明的变量绑定到当前作用域。
  • 块级作用域内的变量在外部不可访问:
    {
      let blockVar = "内部";
    }
    console.log(blockVar); // ❌ ReferenceError

应用场景

  • 循环中的计数器隔离。
  • 条件语句中的临时变量保护。
  • 避免全局污染。

总结对比表

特性varlet/const
作用域函数作用域块级作用域
变量提升✅(值为 undefined❌(TDZ 报错)
重复声明✅ 允许❌ 报错
全局变量绑定✅(window 对象)❌(全局但不在 window

最佳实践

  1. 优先使用 const:除非需要重新赋值。
  2. 避免使用 var:除非需要兼容旧环境。
  3. 利用块级作用域:限制变量生命周期,减少命名冲突。
  4. 注意 TDZ:始终先声明后使用。

通过理解这些机制,可以更好地掌握 JavaScript 的作用域和变量行为,避免常见陷阱。

底层原因

var bar = {
    myName:"11",
    printName: function () {
        console.log(this.myName)
    }    
}
function foo() {
    let myName = "22"
    return bar.printName
}
var myName = "33"
let _printName = foo()
_printName()//33
bar.printName()//11

1. 编译阶段(变量提升与作用域创建)

  • 全局对象(Global Object,浏览器中为window)
    {
      bar: undefined,   // var声明,初始化为undefined
      foo: <Function>,  // 函数声明整体提升
      myName: undefined // var声明,初始化为undefined
    }
  • 词法环境(Lexical Environment)
    {
      _printName: <uninitialized>  // let声明,未初始化(暂时性死区)
    }

2. 执行阶段

  1. 变量赋值与函数执行

    • bar 被赋值为对象:
      bar = { myName: "11", printName: function() {...} }
    • var myName = "33" 执行,全局对象的 myName 变为 "33"
    • foo() 被调用:
      • foo 的函数作用域
        {
          myName: "22"  // let声明,局部变量
        }
      • return bar.printName:返回 bar.printName 的函数引用(未绑定 barthis ,函数在调用时才会绑定this)。
    • _printName 被赋值为 bar.printName 的函数引用。
  2. 关键函数调用分析

    • _printName()
      • 相当于直接调用函数 function() { console.log(this.myName) }
      • 非严格模式下this 指向全局对象(window),因此 this.myName 找到全局的 "33"
    • bar.printName()
      • 作为方法调用,this 指向 bar,因此输出 bar.myName(即 "11")。

  1. varlet 的作用域差异

    • var 声明的变量(如 myName)会挂载到全局对象(window.myName)。
    • let 声明的变量(如 _printName)存在于词法环境中,但不在全局对象上。
  2. this 的指向逻辑

    • 函数调用方式决定 this
      • _printName() 作为独立函数调用,this 默认指向全局对象(非严格模式)。
      • 若在严格模式("use strict")下,thisundefined,会导致报错。
      • bar.printName() 作为方法调用,this 指向 bar
  3. 执行顺序的准确性

    • var myName = "33"foo() 调用之前执行,因此全局的 myName 已为 "33"
    • foo() 内部的 let myName = "22" 是局部变量,不影响全局。

lesson09-块级作用域:var缺陷以及为什么要引入let和const