为什么有闭包?

时间:2021-01-18 22:45:04

之前一直认为写博客是个可有可无的事情,前天一个电话面试问得我手足无措,发现很多以前知道的东西现在只能说出个大概,很久没复习的缘故吧。而转身去看的时候,又不知从何看起,顿时觉得有写博客的必要。与日记不同,说不定路过的哪位大神会指出我的错误呢,有趣的讨论还可以加深理解。

什么是闭包?

这个定义一俩句话说出来还真不容易,而且晦涩。 从字面词来讲的话就是一个包裹起来的封闭的东西。百度百科的解释是:

闭包是指可以包含*(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于*变量被包含在代码块中,这些*变量以及它们引用的对象没有被释放)和为*变量提供绑定的计算环境(作用域)

是不是有点晦涩啊? 哈哈。下面以javascript语言来演示一下闭包:

    var obj=(function(){
var num=0;
return {
"getNum":function(){
return num;
},
"setNum":function(v){
num=v;
}
}
})();
console.log(obj.getNum());// 0
obj.setNum(3);
console.log(obj.getNum());//3

obj的值是一个立即执行的函数的返回值,在这个函数里面定义了一个num变量,初始值为0 。从javascript的语法规则来讲,我们不可能在这个函数外面访问到num,但是在这个例子里面却可以通过obj来访问,如果你用过C,会非常惊讶。这是一个闭包的例子,通过闭包可以将一些东西隐藏起来,只对外暴露想暴露的部分,类似于C++的类,私有成员不能被外部访问。

先给出一个闭包的定义:

A closure is a pair consisting of the function code and the environment in which the function is created.链接

在函数式语言里可以把函数当成值传来传去,甚至可以在运行的时候创建函数。函数运行需要相应的代码和运行环境(用来寻找执行所需要的各种值)。

为什么会有闭包?

a snippet of javascript code

   // environment E1
var x=1;
var createFunc=function (y){
//environment E2
return function(z){
//environment E3
return x+y+z;
}
}
var func=createFunc(10);
console.log(func(100));//111

func的值是一个运行时创建的函数,这个函数运行的时候需要用到三个值(x,y,z),不讨论x和z,主要讨论y。 大部分程序运行时都有一个运行栈,调用一个函数就在栈上放一个
record .
为什么有闭包?
每一个记录上包含存取链(控制数据访问),控制链(函数调用关系,返回地址),局部变量。当函数执行完返回,相应的记录就被删除了。那么按这个逻辑上面那段代码是不可能工作的。但该图所示的原理并不适用所有的语言。javascript也是called-stack,但是有区别。上面代码里标注了E1,E2,E3,代表三个不同的environment。E2的parent environment是E1,E3的parent environment是E2。func执行时寻找所需变量的值是从E3到E2,再到E1。但是对于y这个变量是作为createFunc的参数传入,按照called-stack的运行方式,createFunc一旦返回,相应的记录将从stack上面删除,那么y将不可访问。不同就在于此,当js发现闭包时会将相应的运行时环境保存到heap(堆)上,这里的运行时环境就是上面英文引用中的environment,js中称之为scope chain.所以当func被赋值,不仅其对应的函数代码被保存(可以通过func.toString()查看),相应的环境也被保存,即此时的E3,E2,E1。E2里面保存的是y的值10和指向其外部环境E1的指针。如果再次运行createFunc则会再创建一个独立的E2。代码中的func就是一个闭包(包含相应的code和运行环境),理论上讲,javascript中的所有的函数都是闭包。

上面的闭包定义也适用于其他语言,不仅是javascript。

现在是在重写这篇文章,以补充原文中的纰漏。并重新理一理闭包和GC的关系。闭包是解决函数式语言的一个问题的一种技术,这个问题就是如何保证将函数当做值创造并传来传去的时候函数仍能正确运行。

闭包与GC

(有些语言具有闭包特性但没有GC,暂不讨论。)闭包解决了函数式语言的一个问题,随之而来出现了另一个问题---内存。“environment”保存在了堆上(可能创建很多environment),不再使用之后总需要去释放,这个操作就是由GC负责。GC通过算法(引用计数、跟踪(标记清扫、标记压缩、标记拷贝))来决定是否释放变量占用的相应内存,当他发现一个变量不在被引用则释放相应的内存空间。GC设计的最初目的是将程序员从烦人的内存管理中解放。

上面说的environment或者是作用域链什么的,在底层看来就是用指针串起来的一个个内存块,自然也就是GC管理的对象。“发现闭包并保存其运行环境”这一动作对GC来讲就是将值从stack上复制到heap上,并修改指针;或者有些方式是在编译阶段就检测到闭包,直接就在heap上申请相应的空间存放会被外部引用的变量(variables就构成了environment)。但这只是内存部分的操作,除此还要将运行环境绑定到函数变量上,这个就不是GC的工作了。之前说因为GC的工作原理必然会导致闭包的出现,但是忽略了绑定环境到函数变量这一操作。下面这段Golang代码就不是闭包,如果返回的&z是一个函数或是包含函数那么就构成闭包了:

  func add(x,y int){
z:=x+y
return &z
}
func main(){
result:=add(1,2)
fmt.Println(*result)
}

所以结论是:

闭包是为了解决函数式语言中一个问题的技术,GC是支持闭包实现的一个机制。

发表后

发表后得到网友的意见,长了见识,更正原文几个论述:

  • 一是“闭包是某一类语言的现象”,这个表述不准确,语言设计者也许最初就设计了闭包这一特性,GC只不过支持这一特性的一种机制(GC主要还是用来方便内存管理)
  • 有闭包不一定有GC,感谢Lukexywang的评论,不排除其他实现可能性

最后贴俩个链接:
About closure, LexicalEnvironment and GC
How to implement closures without gc?