闭包就是能够读取其他函数内部变量的函数。由于在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。1
简单介绍闭包包含自由(未绑定到特定对象)变量,这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。在PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective c、swift 以及Java(Java8及以上)等语言中都能找到对闭包不同程度的支持。
本质集合 S 是闭集当且仅当 Cl(S)=S(这里的cl即closure,闭包)。特别的,空集的闭包是空集,X 的闭包是 X。集合的交集的闭包总是集合的闭包的交集的子集(不一定是真子集)。有限多个集合的并集的闭包和这些集合的闭包的并集相等;零个集合的并集为空集,所以这个命题包含了前面的空集的闭包的特殊情况。无限多个集合的并集的闭包不一定等于这些集合的闭包的并集,但前者一定是后者的父集。
若 A 为包含 S 的 X 的子空间,则 S 在 A 中计算得到的闭包等于 A 和 S 在 X 中计算得到的闭包(Cl_A(S) = A ∩ Cl_X(S))的交集。特别的,S在 A 中是稠密的,当且仅当 A 是 Cl_X(S) 的子集。
性质cl(S) 是 S 的闭父集。
cl(S) 是所有包含 S 的闭集的交集。
cl(S) 是包含 S 的最小的闭集。
集合 S 是闭集,当且仅当 S = cl(S)。
若 S 是 T 的子集,则 cl(S) 是 cl(T) 的子集。
若 A 是闭集,则 A 包含 S 当且仅当 A 包含 cl(S)。
有时候,上述第二或第三条性质会被作为拓扑闭包的定义。
在第一可数空间(如度量空间)中,cl(S) 是所有点的收敛数列的所有极限。
举例说明闭包 (closure)是个精确但又很难解释的电脑名词。在 Perl 里面,闭包是以 匿名函数的形式来实现,具有持续参照位于该函数范围之外的文字式变数值的能力。这些外部的文字变数会神奇地保留它们在闭包函数最初定义时的值 (深连结)。
如果一个程式语言容许函数递回另一个函数的话 (像 Perl 就是),闭包便具有意义。要注意的是,有些语言虽提供匿名函数的功能,但却无法正确处理闭包; Python 这个语言便是一例。如果要想多了解闭包的话,建议你去找本功能性程式 设计的教科书来看。Scheme这个语言不仅支持闭包,更鼓励多加使用。
以下是个典型的产生函数的函数:
sub add_function_generator {
return sub { shift + shift };
}
$add_sub = add_function_generator();
$sum = &$add_sub(4,5); # $sum是 9了
闭包用起来就像是个函数样板,其中保留了一些可以在稍後再填入的空格。add_function_generator() 所递回的匿名函数在技术上来讲并不能算是一个闭包, 因为它没有用到任何位在这个函数范围之外的文字变数。
把上面这个例子和下面这个make_adder()函数对照一下,下面这个函数所递回的匿名函数中使用了一个外部的文字变数。这种指明外部函数的作法需要由 Perl递回一个适当的闭包,因此那个文字变数在匿名函数产生之时的值便永久地被锁进闭包里。
sub make_adder {
my $addpiece = shift;
return sub { shift + $addpiece };
}
$f1 = make_adder(20);
$f2 = make_adder(555);
这样一来&$f1($n) 永远会是 20加上你传进去的值$n ,而&$f2($n) 将 永远会是 555加上你传进去的值$n。$addpiece的值会在闭包中保留下来。
闭包在比较实际的场合中也常用得到,譬如当你想把一些程式码传入一个函数时:
my $line;
timeout(30,sub { $line = });
如果要执行的程式码当初是以字串的形式传入的话,即'$line = ' ,那么timeout() 这个假想的函数在回到该函数被呼叫时所在的范围后便无法再截取$line这个文字变数的值了。
语法结构Groovy的闭包闭包(Closure)是Java所不具备的语法结构(JAVA8增加了对闭包的支持)。闭包就是一个代码块,用“{ }”包起来。此时,程序代码也就成了数据,可以被一个变量所引用(与C语言的函数指针比较类似)。闭包的最典型的应用是实现回调函数(callback)。Groovy的API大量使用闭包,以实现对外开放。闭包的创建过程很简单,例如:
{ 参数 ->
代码...
}
参考下面的例子代码,定义了c1和c2两个闭包,并对它们进行调用:
def c1 = { println it }
def c2 = { text -> println text }
c1.call("content1") //用call方法调用闭包
c2("content2") //直接调用闭包
“->;”之前的部分为闭包的参数,如果有多个参数,之间可用逗号分割;“->;”之后的部分为闭包内的程序代码。如果省略了“->;”和它之前的部分,此时闭包中代码,可以用名为“it”的变量访问参数。
闭包的返回值和函数的返回值定义方式是一样的:如果有return语句,则返回值是return语句后面的内容;如果没有return语句,则闭包内的最后一行代码就是它的返回值。2
Lua中的闭包当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征在lua中我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。
下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;想根据学生的成绩从高到低对学生进行排序,可以这样做:
names = {"Peter","Paul","Mary"}
grades = {Mary = 10,Paul = 7,Peter = 8}
table.sort(names,function (n1,n2)
return grades[n1] > grades[n2] -- compare the grades
end)
假定创建一个函数实现此功能:
function sortbygrade (names,grades)
table.sort(names,function (n1,n2)
return grades[n1] > grades[n2] --compare the grades
end)
Scheme中的闭包其他编程的语言主要采用的是闭包的第二种意义(一个与闭包毫不相干的概念):闭包也算一种为表示带有自由变量的过程而用的实现技术。但Scheme的术语“闭包”来自抽象代数。在抽象代数里,一集元素称为在某个运算(操作)之下封闭,如果将该运算应用于这一集合中的元素,产生出的仍然是该集合里的元素。
用Scheme的序对举例,为了实现数据抽象,Scheme提供了一种称为序对的复合结构。这种结构可以通过基本过程cons构造出来。过程cons取两个参数,返回一个包含这两个参数作为其成分的复合数据对象。请注意,一个序对也算一个数据对象。进一步说,还可以用cons去构造那种其元素本身就是序对的序对,并继续这样做下去。
(define x (cons 1 2)) //构造一个x序对,由1,2组成
(define y (cons 3 4))
(define z (cons x y))
Scheme可以建立元素本身也算序对的序对,这就是表结构得以作为一种表示工具的根本基础。我们将这种能力称为cons的闭包性质。一般说,某种组合数据对象的操作满足闭包性质,那就是说,通过它组合起数据对象得到的结果本身还可以通过同样的操作再进行组合。闭包性质是任何一种组合功能的威力的关键要素,因为它使我们能够建立起层次性结构,这种结构由一些部分构成,而其中的各个部分又是由它们的部分构成,并且可以如此继续下去。
闭包的创建在Javascript中闭包的创建过程,如以下程序所示。
代码function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}var c=a();
c();
特点这段代码有两个特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。
这样在执行完var c=a( )后,变量c实际上是指向了函数b,再执行c( )后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,这是因为函数a外的变量c引用了函数a内的函数b。也就是说,当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。
作用简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。
在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。
那 么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引 用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被回收。
例子例1模拟私有变量:
function Counter(start){
var count = start;
return{
increment:function(){
count++;
},
get:function(){
return count;
}
}
}
var foo =Counter(4);
foo.increment();
foo.get();// 5
这里,Counter 函数返回两个闭包,函数 increment 和函数 get。 这两个函数都维持着对外部作用域 Counter 的引用,因此总可以访问此作用域内定义的变量count。
objective c的闭包(block)
objective c 中的的闭包,是通过block实现的。Apple在C,Objective-C和C++中扩充了Block这种文法的,并且在GCC4.2中进行了支持。你可以把它理解为函数指针,匿名函数,闭包,lambda表达式,这里暂且用块对象来表述,因为它们之间还是有些许不同的。
如果以内联方式使用块对象,则无需声明。块对象声明语法与函数指针声明语法相似,但是块对象应使用脱字符(^)而非星号指针 (*)。下面的代码声明一个aBlock变量,它标识一个需传入三个参数并具有float返回值的块。
float (^aBlock)(const int*, int, float);
例2下面是一个使用闭包简单的例子,模拟一个计数器,通过将整型包裹为一个列表的单一元素来模拟使看起来更易变:
函数counter()所作的唯一一件事就是接受一个初始化的值来计数,并将该值赋给列表count成员,然后定义一个内部函数incr()。通过内部函数使用变量count,就创建了一个闭包。最魔法的地方是counter()函数返回一个incr(),一个可以调用的函数对象。
运行:
>>> c = counter⑸
>>> type(c)
>>> print c()6
>>> print c()
7
>>> c2 = counter(99)
100
>>> print c()
8
微观世界如 果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(execution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
1、当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
2、当函数a执行的时候,a会进入相应的执行环境(execution context)。
3、在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
4、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
5、下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
6、最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果函数b存在prototype原型对象,则在查找完自身的活动对象 后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
应用场景1、保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
2、在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
以上两点是闭包最基本的应用场景,很多经典案例都源于此。
回收机制在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
匿名内部在Python中的闭包(Closure)
学过Java GUI编程的人都知道定义匿名内部类是注册监听等处理的简洁有效手段,闭包的定义方式有点类似于这种匿名内部类,
但是闭包的作用威力远远超过匿名内部类,这也是很多流行动态语言选择闭包的原因,相信你在JavaScript中已经了解它的神奇功效了。
度量空间中闭包对欧几里德空间的子集 S,x 是 S 的闭包点,若所有以 x 为中心的开球都包含 S 的点(这个点也可以是 x)。
这个定义可以推广到度量空间 X 的任意子集 S。具体地说,对具有度量 d 的度量空间 X,x 是 S 的闭包点,若对所有 r > 0,存在 y 属于 S,使得距离 d(x,y)
这个定义也可以推广到拓扑空间,只需要用邻域替代“开球”。设 S 是拓扑空间 X 的子集,则 x 是 S 的闭包点,若所有 x 邻域都包含 S 的点。注意,这个定义并不要求邻域是开的。
离散数学中的闭包离散数学中,一个关系R的闭包,是指加上最小数目的有序偶而形成的具有自反性,对称性或传递性的新的有序偶集,此集就是关系R的闭包。
设R是集合A上的二元关系,R的自反(对称、传递)闭包是满足以下条件的关系R':
(i)R'是自反的(对称的、传递的);
(ii)R'⊇R;
(iii)对于A上的任何自反(对称、传递)关系R",若R"⊇R,则有R"⊇R'。
R的自反、对称、传递闭包分别记为r(R)、s(R) 和t(R)。
性质1
集合A上的二元关系R的闭包运算可以复合,例如:
ts(R)=t(s(R))
表示R的对称闭包的传递闭包,通常简称为R的对称传递闭包。而tsr(R)则表示R的自反对称传递闭包。
性质2
设R是集合A上的二元关系,则有
(a)如果R是自反的,那么s(R)和t(R)也是自反的;
(b)如果R是对称的,那么r(R)和t(R)也是对称的;
(c)如果R是传递的,那么r(R)也是传递的。
性质3
设R是集合A上的二元关系,则有
(a)rs(R)=sr(R);
(b)rt(R)=tr(R);
(c)ts(R)⊇ st(R)。
使用闭包的注意点(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
本词条内容贡献者为:
尚华娟 - 副教授 - 上海财经大学