版權(quán)歸原作者所有,如有侵權(quán),請聯(lián)系我們

[科普中國]-閉包

科學(xué)百科
原創(chuàng)
科學(xué)百科為用戶提供權(quán)威科普內(nèi)容,打造知識科普陣地
收藏

閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在javascript中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,所以閉包可以理解成“定義在一個函數(shù)內(nèi)部的函數(shù)“。在本質(zhì)上,閉包是將函數(shù)內(nèi)部和函數(shù)外部連接起來的橋梁。1

簡單介紹閉包包含自由(未綁定到特定對象)變量,這些變量不是在這個代碼塊內(nèi)或者任何全局上下文中定義的,而是在定義代碼塊的環(huán)境中定義(局部變量)?!伴]包” 一詞來源于以下兩者的結(jié)合:要執(zhí)行的代碼塊(由于自由變量被包含在代碼塊中,這些自由變量以及它們引用的對象沒有被釋放)和為自由變量提供綁定的計算環(huán)境(作用域)。在PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective c、swift 以及Java(Java8及以上)等語言中都能找到對閉包不同程度的支持。

本質(zhì)集合 S 是閉集當(dāng)且僅當(dāng) Cl(S)=S(這里的cl即closure,閉包)。特別的,空集的閉包是空集,X 的閉包是 X。集合的交集的閉包總是集合的閉包的交集的子集(不一定是真子集)。有限多個集合的并集的閉包和這些集合的閉包的并集相等;零個集合的并集為空集,所以這個命題包含了前面的空集的閉包的特殊情況。無限多個集合的并集的閉包不一定等于這些集合的閉包的并集,但前者一定是后者的父集。

若 A 為包含 S 的 X 的子空間,則 S 在 A 中計算得到的閉包等于 A 和 S 在 X 中計算得到的閉包(Cl_A(S) = A ∩ Cl_X(S))的交集。特別的,S在 A 中是稠密的,當(dāng)且僅當(dāng) A 是 Cl_X(S) 的子集。

性質(zhì)cl(S) 是 S 的閉父集。

cl(S) 是所有包含 S 的閉集的交集。

cl(S) 是包含 S 的最小的閉集。

集合 S 是閉集,當(dāng)且僅當(dāng) S = cl(S)。

若 S 是 T 的子集,則 cl(S) 是 cl(T) 的子集。

若 A 是閉集,則 A 包含 S 當(dāng)且僅當(dāng) A 包含 cl(S)。

有時候,上述第二或第三條性質(zhì)會被作為拓?fù)溟]包的定義。

在第一可數(shù)空間(如度量空間)中,cl(S) 是所有點的收斂數(shù)列的所有極限。

舉例說明閉包 (closure)是個精確但又很難解釋的電腦名詞。在 Perl 里面,閉包是以 匿名函數(shù)的形式來實現(xiàn),具有持續(xù)參照位于該函數(shù)范圍之外的文字式變數(shù)值的能力。這些外部的文字變數(shù)會神奇地保留它們在閉包函數(shù)最初定義時的值 (深連結(jié))。

如果一個程式語言容許函數(shù)遞回另一個函數(shù)的話 (像 Perl 就是),閉包便具有意義。要注意的是,有些語言雖提供匿名函數(shù)的功能,但卻無法正確處理閉包; Python 這個語言便是一例。如果要想多了解閉包的話,建議你去找本功能性程式 設(shè)計的教科書來看。Scheme這個語言不僅支持閉包,更鼓勵多加使用。

以下是個典型的產(chǎn)生函數(shù)的函數(shù):

sub add_function_generator {

return sub { shift + shift };

}

$add_sub = add_function_generator();

$sum = &$add_sub(4,5); # $sum是 9了

閉包用起來就像是個函數(shù)樣板,其中保留了一些可以在稍後再填入的空格。add_function_generator() 所遞回的匿名函數(shù)在技術(shù)上來講并不能算是一個閉包, 因為它沒有用到任何位在這個函數(shù)范圍之外的文字變數(shù)。

把上面這個例子和下面這個make_adder()函數(shù)對照一下,下面這個函數(shù)所遞回的匿名函數(shù)中使用了一個外部的文字變數(shù)。這種指明外部函數(shù)的作法需要由 Perl遞回一個適當(dāng)?shù)拈]包,因此那個文字變數(shù)在匿名函數(shù)產(chǎn)生之時的值便永久地被鎖進閉包里。

sub make_adder {

my $addpiece = shift;

return sub { shift + $addpiece };

}

$f1 = make_adder(20);

$f2 = make_adder(555);

這樣一來&$f1($n) 永遠(yuǎn)會是 20加上你傳進去的值$n ,而&$f2($n) 將 永遠(yuǎn)會是 555加上你傳進去的值$n。$addpiece的值會在閉包中保留下來。

閉包在比較實際的場合中也常用得到,譬如當(dāng)你想把一些程式碼傳入一個函數(shù)時:

my $line;

timeout(30,sub { $line = });

如果要執(zhí)行的程式碼當(dāng)初是以字串的形式傳入的話,即'$line = ' ,那么timeout() 這個假想的函數(shù)在回到該函數(shù)被呼叫時所在的范圍后便無法再截取$line這個文字變數(shù)的值了。

語法結(jié)構(gòu)Groovy的閉包閉包(Closure)是Java所不具備的語法結(jié)構(gòu)(JAVA8增加了對閉包的支持)。閉包就是一個代碼塊,用“{ }”包起來。此時,程序代碼也就成了數(shù)據(jù),可以被一個變量所引用(與C語言的函數(shù)指針比較類似)。閉包的最典型的應(yīng)用是實現(xiàn)回調(diào)函數(shù)(callback)。Groovy的API大量使用閉包,以實現(xiàn)對外開放。閉包的創(chuàng)建過程很簡單,例如:

{ 參數(shù) ->

代碼...

}

參考下面的例子代碼,定義了c1和c2兩個閉包,并對它們進行調(diào)用:

def c1 = { println it }

def c2 = { text -> println text }

c1.call("content1") //用call方法調(diào)用閉包

c2("content2") //直接調(diào)用閉包

“->;”之前的部分為閉包的參數(shù),如果有多個參數(shù),之間可用逗號分割;“->;”之后的部分為閉包內(nèi)的程序代碼。如果省略了“->;”和它之前的部分,此時閉包中代碼,可以用名為“it”的變量訪問參數(shù)。

閉包的返回值和函數(shù)的返回值定義方式是一樣的:如果有return語句,則返回值是return語句后面的內(nèi)容;如果沒有return語句,則閉包內(nèi)的最后一行代碼就是它的返回值。2

Lua中的閉包當(dāng)一個函數(shù)內(nèi)部嵌套另一個函數(shù)定義時,內(nèi)部的函數(shù)體可以訪問外部的函數(shù)的局部變量,這種特征在lua中我們稱作詞法定界。雖然這看起來很清楚,事實并非如此,詞法定界加上第一類函數(shù)在編程語言里是一個功能強大的概念,很少語言提供這種支持。

下面看一個簡單的例子,假定有一個學(xué)生姓名的列表和一個學(xué)生名和成績對應(yīng)的表;想根據(jù)學(xué)生的成績從高到低對學(xué)生進行排序,可以這樣做:

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)

假定創(chuàng)建一個函數(shù)實現(xiàn)此功能:

function sortbygrade (names,grades)

table.sort(names,function (n1,n2)

return grades[n1] > grades[n2] --compare the grades

end)

Scheme中的閉包其他編程的語言主要采用的是閉包的第二種意義(一個與閉包毫不相干的概念):閉包也算一種為表示帶有自由變量的過程而用的實現(xiàn)技術(shù)。但Scheme的術(shù)語“閉包”來自抽象代數(shù)。在抽象代數(shù)里,一集元素稱為在某個運算(操作)之下封閉,如果將該運算應(yīng)用于這一集合中的元素,產(chǎn)生出的仍然是該集合里的元素。

用Scheme的序?qū)εe例,為了實現(xiàn)數(shù)據(jù)抽象,Scheme提供了一種稱為序?qū)Φ膹?fù)合結(jié)構(gòu)。這種結(jié)構(gòu)可以通過基本過程cons構(gòu)造出來。過程cons取兩個參數(shù),返回一個包含這兩個參數(shù)作為其成分的復(fù)合數(shù)據(jù)對象。請注意,一個序?qū)σ菜阋粋€數(shù)據(jù)對象。進一步說,還可以用cons去構(gòu)造那種其元素本身就是序?qū)Φ男驅(qū)?,并繼續(xù)這樣做下去。

(define x (cons 1 2)) //構(gòu)造一個x序?qū)?,?,2組成

(define y (cons 3 4))

(define z (cons x y))

Scheme可以建立元素本身也算序?qū)Φ男驅(qū)Γ@就是表結(jié)構(gòu)得以作為一種表示工具的根本基礎(chǔ)。我們將這種能力稱為cons的閉包性質(zhì)。一般說,某種組合數(shù)據(jù)對象的操作滿足閉包性質(zhì),那就是說,通過它組合起數(shù)據(jù)對象得到的結(jié)果本身還可以通過同樣的操作再進行組合。閉包性質(zhì)是任何一種組合功能的威力的關(guān)鍵要素,因為它使我們能夠建立起層次性結(jié)構(gòu),這種結(jié)構(gòu)由一些部分構(gòu)成,而其中的各個部分又是由它們的部分構(gòu)成,并且可以如此繼續(xù)下去。

閉包的創(chuàng)建在Javascript中閉包的創(chuàng)建過程,如以下程序所示。

代碼function a(){

var i=0;

function b(){

alert(++i);

}

return b;

}var c=a();

c();

特點這段代碼有兩個特點:

1、函數(shù)b嵌套在函數(shù)a內(nèi)部;

2、函數(shù)a返回函數(shù)b。

這樣在執(zhí)行完var c=a( )后,變量c實際上是指向了函數(shù)b,再執(zhí)行c( )后就會彈出一個窗口顯示i的值(第一次為1)。這段代碼其實就創(chuàng)建了一個閉包,這是因為函數(shù)a外的變量c引用了函數(shù)a內(nèi)的函數(shù)b。也就是說,當(dāng)函數(shù)a的內(nèi)部函數(shù)b被函數(shù)a外的一個變量引用的時候,就創(chuàng)建了一個閉包。

作用簡而言之,閉包的作用就是在a執(zhí)行完并返回后,閉包使得Javascript的垃圾回收機制不會收回a所占用的資源,因為a的內(nèi)部函數(shù)b的執(zhí)行需要依賴a中的變量。

在上面的例子中,由于閉包的存在使得函數(shù)a返回后,a中的i始終存在,這樣每次執(zhí)行c(),i都是自加1后alert出i的值。

那 么我們來想象另一種情況,如果a返回的不是函數(shù)b,情況就完全不同了。因為a執(zhí)行完后,b沒有被返回給a的外界,只是被a所引用,而此時a也只會被b引 用,因此函數(shù)a和b互相引用但又不被外界打擾(被外界引用),函數(shù)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 函數(shù)返回兩個閉包,函數(shù) increment 和函數(shù) get。 這兩個函數(shù)都維持著對外部作用域 Counter 的引用,因此總可以訪問此作用域內(nèi)定義的變量count。

objective c的閉包(block)

objective c 中的的閉包,是通過block實現(xiàn)的。Apple在C,Objective-C和C++中擴充了Block這種文法的,并且在GCC4.2中進行了支持。你可以把它理解為函數(shù)指針,匿名函數(shù),閉包,lambda表達(dá)式,這里暫且用塊對象來表述,因為它們之間還是有些許不同的。

如果以內(nèi)聯(lián)方式使用塊對象,則無需聲明。塊對象聲明語法與函數(shù)指針聲明語法相似,但是塊對象應(yīng)使用脫字符(^)而非星號指針 (*)。下面的代碼聲明一個aBlock變量,它標(biāo)識一個需傳入三個參數(shù)并具有float返回值的塊。

float (^aBlock)(const int*, int, float);

例2下面是一個使用閉包簡單的例子,模擬一個計數(shù)器,通過將整型包裹為一個列表的單一元素來模擬使看起來更易變:

函數(shù)counter()所作的唯一一件事就是接受一個初始化的值來計數(shù),并將該值賦給列表count成員,然后定義一個內(nèi)部函數(shù)incr()。通過內(nèi)部函數(shù)使用變量count,就創(chuàng)建了一個閉包。最魔法的地方是counter()函數(shù)返回一個incr(),一個可以調(diào)用的函數(shù)對象。

運行:

>>> c = counter⑸

>>> type(c)

>>> print c()6

>>> print c()

7

>>> c2 = counter(99)

100

>>> print c()

8

微觀世界如 果要更加深入的了解閉包以及函數(shù)a和嵌套函數(shù)b的關(guān)系,我們需要引入另外幾個概念:函數(shù)的執(zhí)行環(huán)境(execution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數(shù)a從定義到執(zhí)行的過程為例闡述這幾個概念。

1、當(dāng)定義函數(shù)a的時候,js解釋器會將函數(shù)a的作用域鏈(scope chain)設(shè)置為定義a時a所在的“環(huán)境”,如果a是一個全局函數(shù),則scope chain中只有window對象。

2、當(dāng)函數(shù)a執(zhí)行的時候,a會進入相應(yīng)的執(zhí)行環(huán)境(execution context)。

3、在創(chuàng)建執(zhí)行環(huán)境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值就為第1步中的scope chain。即a.scope=a的作用域鏈。

4、然后執(zhí)行環(huán)境會創(chuàng)建一個活動對象(call object)?;顒訉ο笠彩且粋€擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創(chuàng)建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。

5、下一步是在活動對象上添加一個arguments屬性,它保存著調(diào)用函數(shù)a時所傳遞的參數(shù)。

6、最后把所有函數(shù)a的形參和內(nèi)部的函數(shù)b的引用也添加到a的活動對象上。在這一步中,完成了函數(shù)b的的定義,因此如同第3步,函數(shù)b的作用域鏈被設(shè)置為b所被定義的環(huán)境,即a的作用域。

到此,整個函數(shù)a從定義到執(zhí)行的步驟就完成了。此時a返回函數(shù)b的引用給c,又函數(shù)b的作用域鏈包含了對函數(shù)a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數(shù)。函數(shù)b被c引用,函數(shù)b又依賴函數(shù)a,因此函數(shù)a在返回后不會被GC回收。

當(dāng)函數(shù)b執(zhí)行的時候亦會像以上步驟一樣。因此,執(zhí)行時b的作用域鏈包含了3個對象:b的活動對象、a的活動對象和window對象,如下圖所示:

當(dāng)在函數(shù)b中訪問一個變量的時候,搜索順序是先搜索自身的活動對象,如果存在則返回,如果不存在將繼續(xù)搜索函數(shù)a的活動對象,依次查找,直到找到為止。如果整個作用域鏈上都無法找到,則返回undefined。如果函數(shù)b存在prototype原型對象,則在查找完自身的活動對象 后先查找自身的原型對象,再繼續(xù)查找。這就是Javascript中的變量查找機制。

應(yīng)用場景1、保護函數(shù)內(nèi)的變量安全。以最開始的例子為例,函數(shù)a中i只有函數(shù)b才能訪問,而無法通過其他途徑訪問到,因此保護了i的安全性。

2、在內(nèi)存中維持一個變量。依然如前例,由于閉包,函數(shù)a中i的一直存在于內(nèi)存中,因此每次執(zhí)行c(),都會給i自加1。

以上兩點是閉包最基本的應(yīng)用場景,很多經(jīng)典案例都源于此。

回收機制在Javascript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那么這兩個互相引用的對象也會被回收。因為函數(shù)a被b引用,b又被a外的c引用,這就是為什么函數(shù)a執(zhí)行后不會被回收的原因。

匿名內(nèi)部在Python中的閉包(Closure)

學(xué)過Java GUI編程的人都知道定義匿名內(nèi)部類是注冊監(jiān)聽等處理的簡潔有效手段,閉包的定義方式有點類似于這種匿名內(nèi)部類,

但是閉包的作用威力遠(yuǎn)遠(yuǎn)超過匿名內(nèi)部類,這也是很多流行動態(tài)語言選擇閉包的原因,相信你在JavaScript中已經(jīng)了解它的神奇功效了。

度量空間中閉包對歐幾里德空間的子集 S,x 是 S 的閉包點,若所有以 x 為中心的開球都包含 S 的點(這個點也可以是 x)。

這個定義可以推廣到度量空間 X 的任意子集 S。具體地說,對具有度量 d 的度量空間 X,x 是 S 的閉包點,若對所有 r > 0,存在 y 屬于 S,使得距離 d(x,y)

這個定義也可以推廣到拓?fù)淇臻g,只需要用鄰域替代“開球”。設(shè) S 是拓?fù)淇臻g X 的子集,則 x 是 S 的閉包點,若所有 x 鄰域都包含 S 的點。注意,這個定義并不要求鄰域是開的。

離散數(shù)學(xué)中的閉包離散數(shù)學(xué)中,一個關(guān)系R的閉包,是指加上最小數(shù)目的有序偶而形成的具有自反性,對稱性或傳遞性的新的有序偶集,此集就是關(guān)系R的閉包。

設(shè)R是集合A上的二元關(guān)系,R的自反(對稱、傳遞)閉包是滿足以下條件的關(guān)系R':

(i)R'是自反的(對稱的、傳遞的);

(ii)R'?R;

(iii)對于A上的任何自反(對稱、傳遞)關(guān)系R",若R"?R,則有R"?R'。

R的自反、對稱、傳遞閉包分別記為r(R)、s(R) 和t(R)。

性質(zhì)1

集合A上的二元關(guān)系R的閉包運算可以復(fù)合,例如:

ts(R)=t(s(R))

表示R的對稱閉包的傳遞閉包,通常簡稱為R的對稱傳遞閉包。而tsr(R)則表示R的自反對稱傳遞閉包。

性質(zhì)2

設(shè)R是集合A上的二元關(guān)系,則有

(a)如果R是自反的,那么s(R)和t(R)也是自反的;

(b)如果R是對稱的,那么r(R)和t(R)也是對稱的;

(c)如果R是傳遞的,那么r(R)也是傳遞的。

性質(zhì)3

設(shè)R是集合A上的二元關(guān)系,則有

(a)rs(R)=sr(R);

(b)rt(R)=tr(R);

(c)ts(R)? st(R)。

使用閉包的注意點(1)由于閉包會使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會造成網(wǎng)頁的性能問題,在IE中可能導(dǎo)致內(nèi)存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除。

(2)閉包會在父函數(shù)外部,改變父函數(shù)內(nèi)部變量的值。所以,如果把父函數(shù)當(dāng)作對象(object)使用,把閉包當(dāng)作它的公用方法(Public Method),把內(nèi)部變量當(dāng)作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數(shù)內(nèi)部變量的值。

本詞條內(nèi)容貢獻(xiàn)者為:

尚華娟 - 副教授 - 上海財經(jīng)大學(xué)