← 返回首页

变量提升、作用域、闭包原理与场景

这三个概念是 JavaScript 核心机制的基石,它们共同构成了 JS 的执行上下文模型。理解它们不仅能帮你应对面试,更能让你写出没有 Bug 的优雅代码。

一、 作用域(Scope):变量的“活动范围”

作用域决定了代码中变量和其他资源的可见性和生命周期。

  • 全局作用域:在任何函数或代码块外部声明的变量,整个程序都能访问。
  • 函数作用域:在函数内部声明的变量,仅在函数内部有效。
  • 块级作用域(ES6 新增):使用 let 和 const 声明的变量,仅在 {} 代码块内部有效,解决了 var 带来的变量泄露问题。

二、 变量提升(Hoisting):代码的“预编译阶段”

JavaScript 引擎在执行代码前,会先进行解析。在这个阶段,它会将变量的声明和函数的声明“提升”到当前作用域的顶部。

  • var 的提升:声明会被提升,但赋值不会。
    console.log(a); // undefined (不会报错,因为声明被提升了)
    var a = 10;     // 相当于 var a; 被提升到了这里
    
  • let / const 的提升:它们同样会被提升,但在声明语句执行之前,它们处于**“暂时性死区”(TDZ)**,此时访问会直接报错。
    console.log(b); // ReferenceError: Cannot access 'b' before initialization
    let b = 20;
    
  • 函数声明的提升:整个函数体都会被提升,可以在声明前调用。但函数表达式(如 var fn = function() {})只会提升变量名,不会提升函数体。

三、 闭包(Closure):函数与它诞生时的“词法环境”

闭包原理:当一个内部函数被返回到外部并被调用时,它不仅包含函数本身的代码,还“记住”了它诞生时的外部变量环境。即使外部函数已经执行完毕,其作用域中的变量依然存活在内存中,不会被垃圾回收机制(GC)销毁。

闭包场景:

  1. 数据私有化(封装):模拟面向对象中的私有变量,防止全局变量污染。
    function createCounter() {
        let count = 0; // 外部无法直接修改
        return {
            increment: () => ++count,
            getCount: () => count
        };
    }
    
  2. 函数柯里化与偏函数:固定部分参数,返回一个接收剩余参数的新函数。
  3. 防抖(Debounce)与节流(Throttle):利用闭包保存定时器 ID,确保多次触发时只执行最后一次或按固定频率执行。

经典高频面试题:循环中的闭包陷阱

// 使用 var:输出 5 个 5
for (var i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), 1000);
}

// 使用 let:输出 0, 1, 2, 3, 4
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}

原理解析:var 没有块级作用域,循环结束后全局只有一个 i=5,5个定时器回调引用的都是同一个 i。而 let 具有块级作用域,JS 引擎在底层会为每一次循环迭代创建一个独立的词法环境(相当于隐藏了一个闭包),因此每个定时器回调都能捕获到属于自己的 i 值。