闭包基础知识

时间:2022-03-08 22:40:15

已经在很多地方听说过闭包这个词,但一直没有搞清楚闭包到底是什么。最近学习Go语言时发现,Go也对闭包有支持,所以顺便找了一些文章对闭包进行稍微深入的学习。本文就是我个人的学习总结。

1.什么是闭包

首先引用*的解释:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了*变量(*变量是指除局部变量以外的变量)的函数。这个被引用的*变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。

Peter J. Landin 在1964年将术语 闭包 定义为一种包含环境成分控制成分的实体,用于在他的SECD机器上对表达式求值。

从中我们可以看出,闭包是由函数和与其相关的引用环境组合而成的实体。在实现binding(有的翻译作绑定,在参考资料1中翻译为 深约束),需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。

为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:

  • 函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。
  • 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

那么。一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:

  • 函数是一阶值;
  • 函数可以嵌套定义;
  • 可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体;
  • 允许定义匿名函数。

这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。

2.闭包的表现形式

通过上面的描述,可以简单的认为一个闭包就是一个“捕获”或“携带”了其被生成的环境中、所属的变量范围内所引用的所有变量的函数。以GoJavaScript中的闭包为例,用过例子来进一步理解闭包的意义和作用。

2.1 Go的闭包

Go的匿名函数是一个闭包。

package main

import "fmt"

func main() {
    var j int = 5
    a := func() (func()) {
        var i int = 10
        return func() {
            fmt.Printf("i, j: %d, %d\n", i, j)
        }
    }() //关于这里为什么加括号可参见参考资料4
    
    a()
    j *= 2
    a()
}

/*
输出结果为:
i, j: 10, 5
i, j: 10, 10
*/

在上面的例子中,变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不能被修改,改变j的值以后,再次调用a,发现结果是修改过的值。

在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而无法通过其他途径访问到,因此保证了i的安全性。

另外一个例子:

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

/*
输出结果为:
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

*/

这是A Tour of Go的示例,官方的解释是:the adder function returns a closure. Each closure is bound to its own sum variable.

2.2 JavaScript的闭包

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

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

print(add5(2));  // 结果为:7
print(add10(2)); // 结果为:12

在这个示例中,我们定义了makeAdder(x)函数:带有一个参数x并返回一个新的函数。返回的函数带有一个参数y,并返回x和y的和。

从本质上讲,makeAdder是一个函数工厂,创建将指定的值和它的参数求和的函数,在上面的示例中,我们使用函数工厂创建了两个新函数,一个将其参数和5求和,另一个和10求和。

add5和add10都是闭包。它们共享相同的函数定义,但是保存了不同的环境。在add5的环境中,x为5。而在add10中,x则为10。

3.闭包的应用

闭包可以用优雅的方式来处理一些棘手的问题,常见的应用有:

  1. 加强模块化。闭包有益于模块化编程,它能以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。
  2. 抽象。闭包是数据和行为的组合,这使得闭包具有较好抽象能力,可以通过闭包来模拟面向对象编程。
  3. 简化代码。

当然还有很多其他应用。关于JavaScript中闭包的详细的应用可以参见参考资料5.

4.参考资料

  1. 闭包的概念、形式与应用
  2. 什么是闭包(Closure)?
  3. 什么是闭包,我的理解
  4. Why add “()” after closure body in Golang?
  5. MDN JavaScript指南:闭包
  6. 学习Javascript闭包
  7. 理解Javascript的闭包