文章目录
- 什么是闭包
- 用例
- 不具有一等函数的语言中的闭包
- 闭包可能造成内存泄漏
- ps:数学中的闭包
wiki-closure
百度百科-闭包
在 JS 中闭包为什么叫「闭包」,而不用其它名称命名?
阮一峰-学习Javascript闭包(Closure)
什么是闭包?闭包的作用? 闭包会导致内存泄漏吗?
什么是闭包
闭包(closure
)的概念是在20世纪60年代为λ-微积分中表达式的机械评估而提出的,并在1970年首次作为PAL编程语言的一个语言特征被完全实现,以支持词义范围内的一等函数
现在如果搜索“闭包”,出现的结果中很多都是关于 JavaScript 的,文章中都是以 JavaScript 讲解闭包的特性,以及在 JS 中的应用场景,但是我们应该清楚,闭包最初的出现是在 JS 之前 30 年,那时解决的场景是当时语言 lambda 演算的不足。
闭包的使用与函数是一等对象的语言有关。在此类语言中,函数可以作为参数传递、从函数调用返回、绑定到变量名等,就像字符串和整数等更简单的类型一样。这种是一等对象的函数,也叫一等函数
意思是,如果想使用闭包,需要使用那些能把函数作为参数、返回值、赋值给变量的语言。如js、golang
如果这个(被用作参数或返回值的)函数带有(内部使用了)自由变量,那就会产生一个闭包(就是这个函数)
自由变量:自由变量是指函数中使用的变量,既不是局部变量也不是该函数的参数。在这种情况下,术语非局部变量通常是同义词。
定义它的环境的变量
非局部变量:在编程语言理论中,非局部变量是指不在局部范围内定义的变量。虽然这个术语可以指全局变量,但它主要用于嵌套函数和匿名函数,在这些函数中,有些变量既不在局部范围,也不在全局范围。
结合闭包理解:自由变量,不是定义在闭包函数中的局部变量,也不是任何全局变量。而是指闭包所在函数的局部变量、所在函数的上级函数的局部变量(如果嵌套了多层函数的话)…
所以,闭包就是能让外部读取函数内部变量的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁
闭包一词可以理解为:在一个封闭的作用域中,将某些自由变量包在定义它的函数中。
用例
在具有一等函数的语言中才能够使用闭包。如果你从其他类型的语言转过来,会觉得一等函数用起来非常爽
“闭包的作用是让外部读取函数内部的变量”,简单的一句话不能让你领悟闭包的魅力,我们可以从以下例子来循序渐进地感受
网上都是用js举例,这里我用go来举例
eg1:n是f1的局部变量,是f2的自由变量。f2捕获了变量n,并被作为返回值传递了出去,外部(相对于n所在的域f1来说)就可以通过f2来访问f1的变量
func f1() func() {
var n = 999
f2 := func() {
fmt.Println(n) // f2包含了自由变量n,是一个闭包函数。闭包了n
}
n++
return f2 // return一个闭包
}
func main() {
result := f1()
result() // 1000 // 在f1外部,通过闭包间接访问到了f1内部的变量
}
eg2:匿名函数捕获自由变量n,并作为f2的参数传递出去,外部(相对于n所在的域main来说)就可以通过f2来访问f1的变量n
func main() {
var n = 999
f2(func() { // 传递了一个闭包
fmt.Println(n) // f2包含了自由变量n,是一个闭包函数。闭包了n
})
}
func f2(fun func()) {
if ... { // fun可以被按需调用
fun()
}
// f2执行完毕后,fun被回收
}
eg3:闭包经常与回调一起使用,特别是用于事件驱动的程序。这个例子比较贴近于使用场景 (这个例子只描述大体意思,不要深究细节)
// 以下是释放某个持续性技能的一段伪代码
// 有一个技能,是持续性技能,目标死亡后停止释放,并可以获得击杀奖励
// 做法是玩家释放技能时监听目标的死亡事件,如果死亡就停止技能的释放,并获得击杀奖励
func SkillCast(doPlayer, bePlayer *Player, skillId int32, ...) {
// ...
buffCom := doPlayer.GetBuffCom() // 获取buff组件
// 利用 buffCom,判断doPlayer是否死亡、是否被定身等,如果是,则不满足技能释放条件
if buffCom... {
return
}
attrCom := doPlayer.GetAttrCom() // 获取属性组件
// 利用 attrCom,判断doPlayer的魔法值,是否能够抵扣本次技能需要的魔法值,如果不能,则不满足技能释放条件
if attrCom... {
return
}
// ...
bePlayer.SetOnDead(func() { // 注册死亡监听,bePlayer死亡时会回调注册的函数
// 停止技能释放
doPlayer.StopSkillCast(skillId) // 闭包了doPlayer和skillId
// 击杀奖励:添加增益buff
buffCom.Add(...) // 闭包了buffCom
// 击杀奖励:回复100滴血
attrCom.AddHp(...) // 闭包了attrCom
})
// ...
}
// 以下是玩家死亡时的一段代码。当玩家死亡时,会回调注册过来的监听函数
func Dead(player *Player, ...) {
// ...
for _, onDead := range player.GetOnDeads() { // 回调所有注册的死亡监听
onDead()
}
// ...
}
从这个例子更加贴近使用场景,但可能还不足以让你感受到闭包的"爽点"
现在我修改一下上边的代码,取消所有的闭包
func SkillCast(doPlayer, bePlayer *Player, skillId int32, ...) {
// ...
buffCom := doPlayer.GetBuffCom()
if buffCom... {
return
}
attrCom := doPlayer.GetAttrCom()
if attrCom... {
return
}
// ...
bePlayer.SetOnDead(func(doPlayer *Player, skillId int32) {
// 停止技能释放
doPlayer.StopSkillCast(skillId)
// 获取buff组件
buffCom := doPlayer.GetBuffCom()
// 击杀奖励:添加增益buff
buffCom.Add(...)
// 获取属性组件
attrCom := doPlayer.GetAttrCom()
// 击杀奖励:回复100滴血
attrCom.AddHp(...)
})
// ...
}
func Dead(player *Player, ...) {
// ...
// 通过一些方式获取到doPlayer和skillId
doPlayer := ...
skillId := ...
for _, onDead := range player.GetOnDeads() { // 回调所有注册的死亡监听
onDead(doPlayer, skillId)
}
// ...
}
如上修改的代码中,doPlayer、skillId、buffCom、attrCom都不再被闭包,回调函数中使用的doPlayer和skillId是Dead传递过来的,buffCom和attrCom是通过doPlayer获取的。
但为了不闭包,需要多写一些代码用来获取和传递doPlayer和skillId、获取buffCom和attrCom,虽然这样做避免了闭包带来的内存占用,但编码效率变慢了、代码可读性也降低了(代码也不再简洁)。并且因为访问内存的次数变多了,所以执行时间也会变长,也就是时间换空间。
其实很多情况下,被闭包的变量都能通过其它方式访问到,而不是必须使用闭包。
使用闭包的优点是
- 提升编码效率:变量被保存起来没有被销毁,不用通过其它方式获取变量(比如把原本闭包的自由变量定义为全局变量)
- 提高代码可读性:代码简洁
- 提升程序执行效率:空间换时间,所以要做好取舍,如果造成大量的内存占用也是不值得的,但大多数情况不会。
不具有一等函数的语言中的闭包
以Java为例。Java不具有一等函数,所以我们不能使用闭包。
严格来说,我们只是不能写出上述典型的闭包代码,但闭包是存在的。
最典型的莫过于 匿名内部类。java可以将类的实例作为参数进行传递,而匿名内部类可以在创建时编写函数。这样就间接实现了传递函数的作用。以下是通过匿名内部类实现的一个回调
// doPlayer为攻击方,bePlayer为受击方。当受击方血量变化时,为攻击方回复魔法值
Player doPlayer = ... //
Player bePlayer = ...
bePlayer.register(new CombatAttributeListener() { // 注册一个战斗属性监听
@Override
public void onChangeHp(int changeNum) {
// ...
doPlayer.ChangeMp(changeNum); // 闭包了doPlayer
// ...
}
});
再进阶一下,Java普通的类其实就是闭包,类方法中捕获类的成员变量,外部只要实例化这个类,就可以调用方法,从而使用类的内部变量
并且不止Java,任何面向对象的语言的类都是闭包,不过它们一般不把类称为闭包,没为什么,就是种习惯
class Add {
private int x = 2;
public void add(){
int y=3;
x = x+y;
System.out.println(x);
}
}
public static void main(String[] args) {
Add add = new Add();
add.add();
}
闭包可能造成内存泄漏
每个语言都有自己的一套GC(垃圾回收)机制,当分配出去的内存不再被引用时便会回收;内存泄露的根本原因就是你的代码中分配了一些‘顽固的’内存,GC无法进行回收,如果这些’顽固的’内存不停地出现,就会导致后面需要的内存不足,造成泄露。
没使用闭包时,函数执行完,局部变量(假设有个局部变量n)就会被销毁。但如果n被闭包,也就多了个外部引用,只能等这个引用销毁,n才能被销毁,n的内存才能被释放
// f1执行完,n就会被回收
function f1(){
var n=999;
}
----------------------------
function f1(){
var n=999;
function f2(){
n++; // n被闭包
console.log(n)
}
return f2;
}
var result=f1();
result();
// result 所在的代码块执行完,n才会被销毁。或者手动给result赋值为空
所以闭包会延迟变量的存在时间,如果a的外部引用一直不被销毁,a的内存就会始终存在。如果这段程序被调用无数次,内存就会泄漏
为了避免内存泄漏,一定要保证a的外部引用会被销毁。不能出现 闭包变量循环引用 等问题导致无法回收
ps:数学中的闭包
数学中也有 闭包 一词,和计算机科学中的 闭包 并不是一个概念
参见:离散数学中的闭包和计算机语言中的闭包有联系吗?、闭包(数学)