闭包
复习
- 语言运行时:高级语言背后的隐藏服务
- 函数调用与运行栈:局部变量的生命周期
- 作用域:名字可见的范围
TL;DR
- 闭包是“函数 + 它捕获的外部变量”
- 它让函数离开定义位置后,仍能访问那些变量
- 被捕获的变量通常要放到堆上,延长生命周期
- 它是许多语言的重要特性
正文
前面讲函数调用时说过:函数返回后,它在栈上的局部变量就该被销毁了。可有时候,我们偏偏想让一个函数记住它出生地的某些变量——即使已经离开了那里。实现这件事的,就是闭包(closure)。
一个“记仇”的函数
看个例子。一个生成计数器的函数:
函数 造计数器():
count = 0
函数 加一():
count = count + 1
返回 count
返回 加一
c = 造计数器()
c() → 1
c() → 2
奇妙的地方来了:造计数器 已经返回了,可它内部的 count 竟然还“活着”——每次调用 c(),count 都记得上次的值。
这是因为,返回的 加一 不只是一个函数,它还带着自己捕获的环境——这个 count 变量。函数加环境,就是闭包。
捕获的是“变量”,不是“值”
要强调:闭包捕获的是那个变量本身,而不是它某一刻的取值。所以 count 才能在多次调用之间持续累积。
这也解释了为什么闭包的实现有讲究:
- 如果
count还按普通局部变量放在栈上,函数一返回,栈帧就没了,count也无处安放 - 所以,被闭包捕获的变量,通常要“搬到”堆上(或者放进一块专门的环境记录里),让它的生命周期不再受栈帧的束缚
于是运行时要在背后做一件事:判断哪些变量被捕获了,把它们分配到能“活得更久”的地方。 这正是语言运行时的一部分工作。
它是一种“带记忆的函数”
闭包的价值,在于把“函数”从单纯的“一段代码”,升级成了“一段代码 + 一份只属于它的记忆”。这让很多写法变得自然:回调、生成器、函数式风格的数据处理……
而这背后,是编译器和运行时在悄悄解决一个难题:当变量的生命超出了它出生的函数,该把它安置在哪里。 答案,往往就是堆。
思考题 1
闭包“捕获”的是什么?为什么需要它?
思考题 2
为什么闭包往往要求把某些变量放到堆上?
小结
知识点
- 闭包是函数加上它捕获的外部变量
- 闭包捕获的是变量本身,可跨调用保持
- 被捕获的变量生命周期超出栈帧
- 因此常被分配到堆上,由运行时安排
参考资料
- Wikipedia(zh):闭包 (计算机科学):携带环境的函数
- Wikipedia(zh):运行时系统:为闭包等特性提供支持
思考题答案(仅供参考)
思考题 1
它捕获的是函数定义处的外部变量(环境),使函数在离开定义位置之后仍能访问、修改这些变量。没有它,函数返回后这些局部变量会随栈帧销毁,函数就“失忆”了。
思考题 2
因为被捕获的变量的生命周期超出了函数调用:函数已经返回、栈帧已经销毁,但变量还要继续存在供闭包使用。栈上的空间随调用结束就没了,所以必须把它放到堆上(或专门的环境记录)以延长寿命。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪