以前写 Java 的时候,听到前端同学谈论闭包,觉得甚是新奇,后面自己写了一小段时间 JS,虽只学到皮毛,也大概了解到闭包的概念,现在工作常用语言是 Go,很多优雅的代码中总是有闭包的身影,看来不了解个透是不可能的了,本文让我来科普(按照自己水平随便瞎扯)一下:
1、什么是闭包?
在真正讲述闭包之前,我们先铺垫一点知识点:
- 函数式编程
- 函数作用域
- 作用域的继承关系
1.1 前提知识铺垫
1.2.1 函数式编程
函数式编程是一种编程范式,看待问题的一种方式,每一个函数都是为了用小函数组织成为更大的函数,函数的参数也是函数,函数返回的也是函数。我们常见的编程范式有:
- 命令式编程:
- 主要思想为:关注计算机执行的步骤,也就是一步一步告诉计算机先做什么再做什么。
- 先把解决问题步骤规范化,抽象为某种算法,然后编写具体的算法去实现,一般只要支持过程化编程范式的语言,我们都可以称为过程化编程语言,比如 BASIC,C 等。
- 声明式编程:
- 主要思想为:告诉计算机应该做什么,但是不指定具体要怎么做,比如 SQL,网页编程的 HTML,CSS。
- 函数式编程:
- 只关注做什么而不关注怎么做,有一丝丝声明式编程的影子,但是更加侧重于”函数是第一位“的原则,也就是函数可以出现在任何地方,参数、变量、返回值等等。
函数式编程可以认为是面向对象编程的对立面,一般只有一些编程语言会强调一种特定的编程方式,大多数的语言都是多范式语言,可以支持多种不同的编程方式,比如 JavaScript ,Go 等。
函数式编程是一种思维方式,将电脑运算视为函数的计算,是一种写代码的方法论,其实我应该聊函数式编程,然后再聊到闭包,因为闭包本身就是函数式编程里面的一个特点之一。
在函数式编程中,函数是头等对象,意思是说一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。(维基百科)
一般纯函数编程语言是不允许直接使用程序状态以及可变对象的,函数式编程本身就是要避免使用 共享状态,可变状态,尽可能避免产生 副作用。
函数式编程一般具有以下特点:
-
函数是第一等公民:函数的地位放在第一位,可以作为参数,可以赋值,可以传递,可以当做返回值。
-
没有副作用:函数要保持纯粹独立,不能修改外部变量的值,不修改外部状态。
-
引用透明:函数运行不依赖外部变量或者状态,相同的输入参数,任何情况,所得到的返回值都应该是一样的。
1.2.2 函数作用域
作用域(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
通俗易懂的说,函数作用域是指函数可以起作用的范围。函数有点像盒子,一层套一层,作用域我们可以理解为是个封闭的盒子,也就是函数的局部变量,只能在盒子内部使用,成为独立作用域。
函数内的局部变量,出了函数就跳出了作用域,找不到该变量。(里层函数可以使用外层函数的局部变量,因为外层函数的作用域包括了里层函数),比如下面的 innerTmep
出了函数作用域就找不到该变量,但是 outerTemp
在内层函数里面还是可以使用。
不管是任何语言,基本存在一定的内存回收机制,也就是回收用不到的内存空间,回收的机制一般和上面说的函数的作用域是相关的,局部变量出了其作用域,就有可能被回收,如果还被引用着,那么就不会被回收。
1.2.3 作用域的继承关系
所谓作用域继承,就是前面说的小盒子可以继承外层大盒子的作用域,在小盒子可以直接取出大盒子的东西,但是大盒子不能取出小盒子的东西,除非发生了逃逸(逃逸可以理解为小盒子的东西给出了引用,大盒子拿到就可以使用)。一般而言,变量的作用域有以下两种:
-
全局作用域:作用于任何地方
-
局部作用域:一般是代码块,函数、包内,函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
1.2 闭包的定义
“多数情况下我们并不是先理解后定义,而是先定义后理解“,先下定义,读不懂没关系:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。 闭包会随着函数的创建而被同时创建。
一句话表述:
闭 包 = 函 数 + 引 用 环 境 闭包 = 函数 + 引用环境 闭包=函数+引用环境
以上定义找不到 Go语言 这几个字眼,聪明的同学肯定知道,闭包是和语言无关的,不是 JavaScript 特有的,也不是 Go 特有的,而是函数式编程语言的特有的,是的,你没有看错,任何支持函数式编程的语言都支持闭包,Go 和 JavaScript 就是其中之二, 目前 Java 目前版本也是支持闭包的,但是有些人可能认为不是完美的闭包,详细情况文中讨论。
1.3 闭包的写法
1.3.1 初看闭包
下面是一段闭包的代码:
import "fmt"
func main() {
sumFunc := lazySum([]int{
1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
var sum = func() int {
fmt.Println("求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
return sum
}
输出的结果:
先获取函数,不求结果
等待一会
求结果...
结果: 15
可以看出,里面的 sum()
方法可以引用外部函数 lazySum()
的参数以及局部变量,在lazySum()
返回函数 sum()
的时候,相关的参数和变量都保存在返回的函数中,可以之后再进行调用。
上面的函数或许还可以更进一步,体现出捆绑函数和其周围的状态,我们加上一个次数 count
:
import "fmt"
func main() {
sumFunc := lazySum([]int{
1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
return sum
}
上面代码输出什么呢?次数 count
会不会发生变化,count
明显是外层函数的局部变量,但是在内存函数引用(捆绑),内层函数被暴露出去了,执行结果如下:
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
第 2 次求结果...
结果: 15
第 3 次求结果...
结果: 15
结果是 count
其实每次都会变化,这种情况总结一下:
- 函数体内嵌套了另外一个函数,并且返回值是一个函数。
- 内层函数被暴露出去,被外层函数以外的地方引用着,形成了闭包。
此时有人可能有疑问了,前面是lazySum()
被创建了 1 次,执行了 3 次,但是如果是 3 次执行都是不同的创建,会是怎么样呢?实验一下:
import "fmt"
func main() {
sumFunc := lazySum([]int</