Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

闭包

复习

  • 语言运行时:高级语言背后的隐藏服务
  • 函数调用与运行栈:局部变量的生命周期
  • 作用域:名字可见的范围

TL;DR

  • 闭包是“函数 + 它捕获的外部变量”
  • 它让函数离开定义位置后,仍能访问那些变量
  • 被捕获的变量通常要放到堆上,延长生命周期
  • 它是许多语言的重要特性

正文

  前面讲函数调用时说过:函数返回后,它在栈上的局部变量就该被销毁了。可有时候,我们偏偏想让一个函数记住它出生地的某些变量——即使已经离开了那里。实现这件事的,就是闭包(closure)。

一个“记仇”的函数

  看个例子。一个生成计数器的函数:

函数 造计数器():
    count = 0
    函数 加一():
        count = count + 1
        返回 count
    返回 加一

c = 造计数器()
c()   → 1
c()   → 2

  奇妙的地方来了:造计数器 已经返回了,可它内部的 count 竟然还“活着”——每次调用 c()count 都记得上次的值。

  这是因为,返回的 加一 不只是一个函数,它还带着自己捕获的环境——这个 count 变量。函数加环境,就是闭包

捕获的是“变量”,不是“值”

  要强调:闭包捕获的是那个变量本身,而不是它某一刻的取值。所以 count 才能在多次调用之间持续累积。

  这也解释了为什么闭包的实现有讲究:

  • 如果 count 还按普通局部变量放在栈上,函数一返回,栈帧就没了,count 也无处安放
  • 所以,被闭包捕获的变量,通常要“搬到”堆上(或者放进一块专门的环境记录里),让它的生命周期不再受栈帧的束缚

  于是运行时要在背后做一件事:判断哪些变量被捕获了,把它们分配到能“活得更久”的地方。 这正是语言运行时的一部分工作。

它是一种“带记忆的函数”

  闭包的价值,在于把“函数”从单纯的“一段代码”,升级成了“一段代码 + 一份只属于它的记忆”。这让很多写法变得自然:回调、生成器、函数式风格的数据处理……

  而这背后,是编译器和运行时在悄悄解决一个难题:当变量的生命超出了它出生的函数,该把它安置在哪里。 答案,往往就是堆。

思考题 1

  闭包“捕获”的是什么?为什么需要它?

思考题 2

  为什么闭包往往要求把某些变量放到堆上?

小结

知识点

  • 闭包是函数加上它捕获的外部变量
  • 闭包捕获的是变量本身,可跨调用保持
  • 被捕获的变量生命周期超出栈帧
  • 因此常被分配到堆上,由运行时安排

参考资料

  1. Wikipedia(zh):闭包 (计算机科学):携带环境的函数
  2. Wikipedia(zh):运行时系统:为闭包等特性提供支持

思考题答案(仅供参考)

思考题 1

  它捕获的是函数定义处的外部变量(环境),使函数在离开定义位置之后仍能访问、修改这些变量。没有它,函数返回后这些局部变量会随栈帧销毁,函数就“失忆”了。

思考题 2

  因为被捕获的变量的生命周期超出了函数调用:函数已经返回、栈帧已经销毁,但变量还要继续存在供闭包使用。栈上的空间随调用结束就没了,所以必须把它放到堆上(或专门的环境记录)以延长寿命。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪