Skip to main content

谈谈闭包

·1986 words·4 mins·
知识库 javascript python
Table of Contents

在学习JS的过程中,我遇到了闭包这个概念,当时并没有在意。直到最近我开始自学python,在廖雪峰老师的python教程中又一次看到了这个名词,我才意识到闭包其实是一个重要的概念,或者说特性,许多高级语言支持闭包(比如近些年比较火的Go语言)。于是我查看了相关文档、教程,打算谈谈我对闭包的一些认识。

闭包的定义
#

闭包有许多不同的定义,个人认为最简洁而达意的是MDN对于闭包的定义:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。

词法环境
#

维基百科这样描述闭包中的词法环境:

环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。

简单来说,词法环境包含两部分:

  • 环境记录:存储符号-值对
  • 对外部环境的引用:对父级词法环境的引用。

也就是说,一个函数的词法环境包含了在函数中的符号定义和函数外部的词法环境。考虑如下python代码:

def init():
    name = "BeaCox"
    def displayName():
        greeting = "Hello"
        print(greeting+', '+name)
    displayName()
init()

displayName函数的词法环境包含了环境记录(greeting的符号-值对)以及对外部环境的引用(namedisplayName的符号-值对),这也就是在displayName函数中可以访问name变量的原因。执行displayName函数,其实就是创建了一个闭包。

使用闭包
#

看完上面的例子,好像有点迷糊了:这不就是“内层作用域可以访问外层作用域的变量”吗?C++不支持闭包,不也能完成上面的工作吗?这是因为上面的例子并没有展示出闭包函数与词法环境捆绑的特性。将上面的代码稍加改动:

def init():
    name = "BeaCox"
    def displayName():
        greeting = "Hello"
        print(greeting+', '+name)
    return displayName
outsideDisplay=init()
outsideDisplay()

这段代码与上面不同的地方在于,displayName函数并不在init函数中执行,而是作为返回值,在init函数外部,有一个outsideDisplay接收了这个返回值。 如果我们从C++的思想来考虑这段代码,会发现:在init函数执行完后,局部变量name已经被回收,这时候outsideDisplayname变量是没有被定义的,这段代码应该不能正常运行。 然而,我们运行这段python程序后会发现,终端正常输出Hello, BeaCox,这就是闭包的魔力!

这段程序之所以正常运行的原因,就是python中返回函数会形成闭包。闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,outsideDisplay 是执行 init 时创建的 displayName 函数实例的引用。displayName 函数和其捆绑的词法环境(变量 name 存在于其中)的引用形成了一个闭包,因此init函数执行完毕后,该词法环境没有消失,变量name也没有被回收。因此,当 outsideDisplay 被调用时,变量 name 仍然可用,程序能够正确运行。

闭包的用途
#

模拟公有成员函数对私有变量的操作
#

读完上述代码不难发现,outsideDisplay函数在init函数外部调用,但却访问到了init函数内部的变量。这与C++中,调用类的公有成员函数来操作类的私有变量非常相似。与C++不同,python不存在严格意义上的私有变量,python通过以双下划线为开头来命名变量的方式,实现的是一种伪私有变量,它不应该被从外部访问,而不是不能被从外部访问。python、JavaScript等不支持严格私有变量的语言可以通过创建闭包来模拟公有成员函数对私有变量的操作

创建一个生命周期极长的局部变量
#

观察上述例子,outsideDisplay函数可以继续重复运行,直到整个程序终止。也就是说name变量直到程序运行结束之前,都一直存在于内存中。听起来貌似很像全局变量,但这个变量却是一个局部变量。仅这个程序而言,这个变量只能被outsideDisplay函数和init函数访问。

闭包可能导致的问题
#

内存泄漏
#

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

上文提到,闭包可以创建一个生命周期极长(直到程序运行结束前始终留存在内存中)的变量,如果这样的变量过多,就会导致程序运行速度减慢甚至系统崩溃。

在循环中创建闭包导致意料之外的错误
#

廖雪峰老师的python教程中给出了一个这样的例子:

def count():
    fs = []
    for i in range(1, 4)://i从1到3
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

这段程序的期望目标是f1, f2, f3分别返回1,4,9。实际返回9, 9, 9。这是因为每个f()函数捆绑外部词法环境中的i是对i的引用,在return fs之前,i已经变成3了,因此每个f()函数返回的都是3*3

因此,要尽量避免在循环中创建闭包。如若必需,务必要谨慎!

BeaCox
Author
BeaCox
Stay humble, remain critical.

Related

《深入理解计算机系统》第7章:链接 阅读报告
·3521 words·8 mins
知识库 CSAPP 系统