Skip to main content

作用域和闭包

作用域(Scope)和闭包(Closure)是 JavaScript 的核心概念,理解它们是掌握 JS 动态特性的关键。以下是深入浅出的解析:

一、作用域(Scope)

1. 定义与分类

  • 作用域:变量和函数的可访问范围,控制着变量与函数的生命周期。
  • 分类
    • 全局作用域:最外层作用域,所有未定义在函数或块内的变量都属于全局作用域。
    • 函数作用域:函数内部定义的变量只能在函数内部访问(var)。
    • 块级作用域letconst 声明的变量在 {} 内有效(如 iffor 块)。

2. 变量提升(Hoisting)

  • var 声明的变量会被提升到作用域顶部,但赋值不会提升。
  • letconst 存在暂时性死区(TDZ),声明前无法访问。
console.log(a); // undefined(变量提升)
var a = 1;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;

3. 词法作用域(静态作用域)

  • JS 采用词法作用域,函数的作用域在定义时确定,而非调用时。
const x = 10;

function outer() {
console.log(x); // 10(从定义位置查找 x)
}

function inner() {
const x = 20;
outer(); // 输出 10,而非 20
}

inner();

二、闭包(Closure)

1. 定义与本质

  • 闭包:函数与其引用的词法环境的组合。即使函数执行完毕,其作用域内的变量也不会被销毁。
  • 核心条件
    1. 函数嵌套。
    2. 内部函数引用外部函数的变量或参数。

2. 经典示例

function outer() {
const count = 0;

function inner() {
count++; // 引用外部变量
console.log(count);
}

return inner; // 返回闭包
}

const counter = outer();
counter(); // 1
counter(); // 2(保留了上次的状态)

3. 闭包的特性

  • 捕获变量:闭包会捕获其定义时的整个词法环境,而非变量的值。
function createGreeters() {
const greeters = [];
for (var i = 0; i < 3; i++) {
greeters.push(() => console.log(i)); // 捕获变量 i 的引用
}
return greeters;
}

const [g1, g2, g3] = createGreeters();
g1(); // 3(循环结束时 i=3)
g2(); // 3
g3(); // 3

// 使用 let 修复(块级作用域)
for (let i = 0; i < 3; i++) {
// 每次循环创建独立的 i
greeters.push(() => console.log(i));
}

三、闭包的应用场景

1. 数据封装与私有变量

function createCounter() {
let count = 0; // 私有变量

return {
increment() { count++; },
getCount() { return count; }
};
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(无法直接访问私有变量)

2. 函数柯里化(Currying)

function add(a, b) {
return a + b;
}

// 转为柯里化函数
const curriedAdd = a => b => a + b;
curriedAdd(3)(5); // 8

3. 事件处理与回调

function setupButton() {
const text = "Hello";

document.getElementById("btn").addEventListener("click", () => {
console.log(text); // 闭包捕获 text
});
}

4. 防抖与节流(见前文解析)

四、闭包的注意事项

1. 内存泄漏风险

  • 闭包会保留对外部变量的引用,若闭包长期存在(如全局变量),可能导致内存无法释放。
// 错误示例:闭包引用 DOM 导致内存泄漏
function leakMemory() {
const element = document.getElementById("leaky");

// 闭包一直引用 element
document.addEventListener("click", () => {
console.log(element.innerHTML);
});
}

2. 性能优化

  • 避免在循环中创建闭包,若无法避免,使用 let 创建块级作用域。
  • 及时释放不再需要的闭包引用(如置为 null)。

五、作用域链(Scope Chain)

  • 定义:由多个嵌套的作用域组成的链表,用于查找变量和函数。
  • 工作原理:当访问一个变量时,JS 会先在当前作用域查找,若未找到则沿作用域链向上查找,直到全局作用域。
const globalVar = "global";

function outer() {
const outerVar = "outer";

function inner() {
const innerVar = "inner";
console.log(innerVar); // 自身作用域
console.log(outerVar); // 外层作用域
console.log(globalVar); // 全局作用域
}

inner();
}

outer();

六、常见面试问题

  1. 什么是闭包?为什么需要闭包?

    • 答:闭包是函数与其词法环境的组合,用于实现数据封装、状态保持和函数复用。
  2. 闭包会导致内存泄漏吗?如何避免?

    • 答:若闭包长期持有对 DOM 或大对象的引用,可能导致泄漏。应避免不必要的引用,并在不再需要时释放闭包。
  3. 如何理解 JS 的词法作用域?

    • 答:函数的作用域由其定义位置决定,而非调用位置。
  4. 分析以下代码的输出:

    for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
    }
    • 答:输出 3 3 3。因为 var 声明的 i 是函数作用域,所有定时器共享同一个 i,循环结束时 i 已变为 3。使用 let 可修复此问题。

七、总结

  • 作用域决定了变量的可见性和生命周期,JS 有全局、函数和块级作用域。
  • 闭包是 JS 最强大的特性之一,允许函数访问并保留其外部作用域的变量,即使外部函数已执行完毕。
  • 合理使用闭包可实现数据封装、状态管理和高阶函数,但需警惕内存泄漏风险。

理解作用域和闭包是掌握 JS 异步编程、函数式编程和框架原理(如 React Hooks)的基础。