概念

闭包(Closure):一个函数,加上它被定义时所处的环境(作用域中的变量)。

换句话说:函数可以"记住"并访问它被创建时的作用域中的变量,即使在那个作用域已经执行完毕之后。

例子(JavaScript)

function makeCounter() {
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

关键:makeCounter() 执行完毕后,局部变量 count 通常应该被销毁。但因为返回的内部函数引用了它,所以 count 依然存在——每次调用 counter() 都会修改同一个 count。

这就是闭包。

为什么叫"闭包"

"闭包"指的是:函数把(close over)它需要的自由变量捕获(capture)进自己的作用域,形成一个封闭的实体。

用途

一、数据封装(模拟私有变量)

function createBankAccount(initial) {
    let balance = initial;  // 外部无法直接访问
    return {
        deposit: (amount) => { balance += amount; },
        getBalance: () => balance
    };
}

balance 只能通过返回的方法访问——实现了某种程度的"私有性"。

注意:这是封装,不是安全边界(在浏览器控制台中仍可能通过调试手段访问)。

二、函数工厂

function multiplier(factor) {
    return (x) => x * factor;
}
const double = multiplier(2);
const triple = multiplier(3);

double 和 triple 是"记住"了不同 factor 的函数。

三、回调函数与事件处理

闭包让回调函数能"携带"上下文:

button.addEventListener('click', () => {
    // 这里可以访问定义时的变量
    console.log('User ' + userName + ' clicked');
});

四、延迟执行(部分求值、柯里化)

function add(a) {
    return (b) => a + b;
}
const addFive = add(5);
addFive(3); // 8

五、模块模式

在 ES Module 普及之前,JavaScript 常用 IIFE(立即执行函数表达式)+ 闭包来实现模块:

const MyModule = (function() {
    let privateVar = 0;
    function privateFn() { ... }
    return { publicFn: ... };
})();

注意事项

一、内存占用

闭包会让被引用的变量无法被垃圾回收——只要闭包还活着,它捕获的环境就一直存在。

如果闭包捕获了很大的对象,且闭包生命周期很长,可能造成内存占用。

二、循环中的经典陷阱

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(而不是 0, 1, 2)

原因:var 是函数作用域,三个闭包共享同一个 i。循环结束时 i 已经是 3。

解决:

  • 用 let(块级作用域,每次循环创建新的绑定);
  • 或用 IIFE 创建新的作用域。

三、Python 中的闭包

Python 支持闭包,但有一个常见陷阱:

funcs = []
for i in range(3):
    funcs.append(lambda: i)
[f() for f in funcs]  # [2, 2, 2]

原因:Python 的闭包是晚绑定(late binding)——函数在调用时才查找变量的值,此时循环已结束。

解决:用默认参数捕获当前值:

funcs.append(lambda i=i: i)

一句话

闭包的本质是函数 + 其定义环境。它让函数拥有了"状态"——这是函数式编程中非常重要的能力。

说明:本文为编程基础的知识整理。