Skip to content

闭包

闭包就是“一个记住了外部变量的函数”。

比如一个函数已经执行完了,按理说它里面的局部变量也该消失了;但如果它返回出去的另一个函数还在用这些变量,那么这些变量就会继续存在。它返回出去的那个函数就叫闭包。

JavaScript 中的闭包

JavaScript 里闭包非常常见,很多回调、工厂函数、模块封装,本质上都在用它。

例一:返回一个捕获局部变量的函数

function closureFunc() {
  let counter = 0;

  function addFunc() {
    counter += 1;
    return counter;
  }

  return addFunc;
}

const add = closureFunc();

console.log(add()); // 1
console.log(add()); // 2
console.log(add()); // 3

这里返回的 addFunc 记住了外层作用域里的 counter。

虽然 closureFunc 已经执行结束了,但 counter 没有消失,因为 addFunc 还在用它。所以每次调用 add,都是在上一次的基础上继续加 1。

例二:根据不同参数创建不同函数

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

这里的 add5 和 add10 都是闭包。

它们虽然来自同一个 makeAdder,但各自记住了不同的 x,所以一个总是在 5 的基础上加,一个总是在 10 的基础上加。

例三:用闭包封装私有状态

const counter = (function () {
  let privateCounter = 0;

  function changeBy(value) {
    privateCounter += value;
  }

  return {
    increment() {
      changeBy(1);
    },
    decrement() {
      changeBy(-1);
    },
    value() {
      return privateCounter;
    }
  };
})();

console.log(counter.value()); // 0
counter.increment();
counter.increment();
console.log(counter.value()); // 2
counter.decrement();
console.log(counter.value()); // 1

这种写法很适合拿来封装内部状态。

外部代码拿不到 privateCounter,只能通过 increment、decrement、value 这些接口间接操作它。很多早期 JavaScript 模块化写法,本质上就是在利用闭包做“私有变量”。

C 语言中的情况

标准 C 并不直接支持闭包。如果想表达类似的效果,常见做法是:

  1. 使用函数指针,状态通过参数从外部传递进来。
  2. 把状态放到结构体里,再把结构体指针显式传给函数。

例如下面这种“函数 + 上下文”的设计,就是 C 里很常见的替代方案:

#include <stdio.h>

typedef struct {
    int counter;
} Context;

int add(Context* ctx) {
    ctx->counter += 1;
    return ctx->counter;
}

int main() {
    Context ctx = {0};

    printf("%d\n", add(&ctx));
    printf("%d\n", add(&ctx));
    return 0;
}

这不是严格意义上的闭包,而是程序员自己把状态手动传给函数。

很多 C 风格库里的回调接口,其实都是这个思路:除了传一个函数指针,再额外传一个 user_data 或 context 指针。

C++ 中的闭包

C++ 里对闭包的支持就自然很多了,因为 lambda 可以直接捕获外部变量。

#include <iostream>

int main() {
    int base = 5;
    auto add_base = [base](int x) {
        return x + base;
    };

    std::cout << add_base(3) << '\n';
    return 0;
}

这里的 lambda 捕获了外部变量 base,所以 add_base 就是一个闭包对象。你可以把它理解成“一个函数,顺手记住了 base 的值”。

如何修改闭包中的状态

如果是按值捕获,那么默认不能修改捕获到的那份副本;如果确实想改,可以加上 mutable:

#include <iostream>

int main() {
    int counter = 0;

    auto next = [counter]() mutable {
        counter += 1;
        return counter;
    };

    std::cout << next() << '\n';
    std::cout << next() << '\n';
    return 0;
}

如果你想改的是外部变量本身,那就要按引用捕获:

#include <iostream>

int main() {
    int counter = 0;

    auto next = [&counter]() {
        counter += 1;
        return counter;
    };

    std::cout << next() << '\n';
    std::cout << next() << '\n';
    std::cout << counter << '\n';
    return 0;
}

函数对象

函数对象是指可以像函数一样被调用的对象。

通常做法是定义一个类或结构体,然后给它实现函数调用运算符operator(),这样一来,这个对象就能像函数一样被调用。

#include <iostream>

class Adder {
  int base;

  int operator()(int x) const {
    return x + base;
  }
};

int main() {
  Adder add5{5};
  Adder add10{10};

  std::cout << add5(2) << '\n';
  std::cout << add10(2) << '\n';
  return 0;
}

这个例子里,add5 和 add10 都是对象,但因为实现了 operator(),所以可以写成 add5(2) 这样的调用形式。

它和前面的闭包例子很像:base 被保存在对象内部,因此不同对象可以带着不同状态工作。

如果函数对象内部的状态需要变化,也可以直接改成员变量:

#include <iostream>

class Counter {
  int value = 0;

  int operator()() {
    value += 1;
    return value;
  }
};

int main() {
  Counter next;

  std::cout << next() << '\n';
  std::cout << next() << '\n';
  std::cout << next() << '\n';
  return 0;
}