闭包
闭包就是“一个记住了外部变量的函数”。
比如一个函数已经执行完了,按理说它里面的局部变量也该消失了;但如果它返回出去的另一个函数还在用这些变量,那么这些变量就会继续存在。它返回出去的那个函数就叫闭包。
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 并不直接支持闭包。如果想表达类似的效果,常见做法是:
- 使用函数指针,状态通过参数从外部传递进来。
- 把状态放到结构体里,再把结构体指针显式传给函数。
例如下面这种“函数 + 上下文”的设计,就是 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;
}