```html
欢迎来到 js面试题 闭包 专业解析站 | 域名:www.kaoacca.cn

一、 什么是 闭包?

在MDN(Mozilla Developer Network)中,闭包被定义为:“函数和其周围状态(词法环境)的引用捆绑在一起形成闭包。可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员。”

通俗来说,闭包就是一个“有记忆”的函数。当一个函数能够记住并访问它被创建时所处的作用域,即使该函数在其原始作用域之外执行,这种行为就称为闭包。

1.1 核心三要素

1.2 基础代码示例

function outer() {
    let count = 0; // 外部函数变量
    // 内部函数,访问了 count
    function inner() {
        count++;
        console.log(count);
    }
    return inner; // 返回内部函数引用
}
const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2
// 此时 outer 的作用域并未被销毁,因为 inner 引用了它

二、 闭包的底层原理与内存管理

理解闭包的关键在于理解JavaScript的作用域链(Scope Chain)和垃圾回收机制(Garbage Collection)。

2.1 作用域链

当JavaScript引擎执行代码时,会创建一个执行上下文栈(Call Stack)。每个函数执行时,都会创建一个包含其局部变量、参数和外部函数引用的对象。如果内部函数引用了外部函数的变量,这些变量就不会被垃圾回收器回收,而是被保存在一个特殊的作用域链中,供内部函数随时访问。

2.2 内存泄漏风险

由于闭包会持有外部函数的变量,如果不当使用,可能导致内存占用过高。例如,在循环中创建大量闭包,或者在不需要时未及时解除引用,都会导致内存无法释放。

✅ 正常生命周期

函数执行完毕,局部变量若无引用,则被GC回收。内存释放。

⚠️ 闭包生命周期

内部函数引用外部变量,即使外部函数执行结束,变量仍驻留内存。

三、 闭包在面试中的高频应用场景

面试官不仅考察你是否知道闭包的定义,更考察你能否在实际开发中运用它。以下是几个经典场景:

3.1 模块模式与数据私有化

在ES6模块出现之前,闭包是实现“私有变量”的主要方式。通过返回一个包含公共方法的对象,可以隐藏内部实现细节,防止外部直接修改状态。

function createBankAccount(initialBalance) {
    let balance = initialBalance; // 私有变量
    return {
        deposit: function(amount) {
            balance += amount;
            return balance;
        },
        withdraw: function(amount) {
            if (amount > balance) return 'Insufficient funds';
            balance -= amount;
            return balance;
        },
        getBalance: function() {
            return balance;
        }
    };
}
const myAccount = createBankAccount(100);
console.log(myAccount.getBalance()); // 100
// myAccount.balance 是 undefined,外部无法直接访问

3.2 函数柯里化(Currying)

柯里化是将接受多个参数的函数转变为一系列接受单个参数的函数的技术。闭包在这里起到了“记忆”之前参数的作用。

function curryAdd(x) {
    return function(y) {
        return function(z) {
            return x + y + z;
        };
    };
}
// 或者更常见的链式调用形式
function add(x) {
    return function(y) {
        return x + y;
    };
}
const add5 = add(5);
console.log(add5(3)); // 8
console.log(add5(10)); // 15

3.3 解决循环中的异步问题

这是面试中最经典的题目之一。在ES6之前,使用 `var` 定义的循环变量在 `setTimeout` 中会输出相同的结果,因为所有定时器共享同一个变量作用域。使用闭包(立即执行函数 IIFE)可以捕获每次循环的变量值。

// ❌ 错误示范:输出 5, 5, 5, 5, 5
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
// ✅ 正确示范:使用闭包捕获 i
for (var i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 1000);
    })(i);
}

四、 闭包的常见陷阱与误区

虽然闭包功能强大,但滥用会导致性能问题和逻辑错误。

4.1 循环中的变量共享

如果在循环中定义函数但未正确捕获变量,所有函数将引用循环结束后的最后一个值。

问题现象 原因分析 解决方案
循环内 setTimeout 输出错误值 var 声明的变量具有函数作用域,所有闭包共享同一个变量 使用 let 块级作用域 或 IIFE 闭包捕获
内存占用持续增长 大量闭包持有大对象引用,GC 无法回收 及时将变量置为 null,减少不必要的闭包
意外修改外部状态 内部函数直接修改了外部可变对象 使用深拷贝或不可变数据结构

4.2 性能损耗

每次调用创建闭包都会分配新的内存空间。在高性能要求的场景(如游戏循环、高频动画)中,应避免在内部函数中创建不必要的闭包。

五、 闭包的性能优化策略

为了在保证功能的同时优化性能,可以采取以下策略:

5.1 使用 WeakMap 优化示例

const privateData = new WeakMap();
class Counter {
    constructor() {
        privateData.set(this, { count: 0 });
    }
    increment() {
        const data = privateData.get(this);
        data.count++;
        console.log(data.count);
    }
}
// 当 Counter 实例被销毁时,WeakMap 中的键值对也会被自动清理
```
斌知网
蜀ICP备2026017620号-5