如果一個函數傳回另一個函數,而被傳回函數又需要外層函數的變量時,不會立即釋放這個變量,而是允許被傳回的函數引用這些變量。支援這種機制的語言稱為支援閉包機制,而這個内部函數連同其自由變量就形成了一個閉包。
如果google一下“閉包”這個詞,會發現網上關于閉包的文章已經不計其數,甚至很多人将閉包看做面試javascript程式員的必考題(雖然閉包和javascript沒有什麼必然聯系)。既然如此,我為什麼還要寫一篇關于閉包的文章呢?
首先,雖然網上關于閉包的文章甚多,但是很少以較為形式化的角度闡述閉包,而我認為了解閉包的關鍵之一就是從形式化角度了解其涵義;其次,大多數文章将閉包的概念與javascript語言綁定太死,這樣容易局限對閉包概念的了解,而難以窺探到其本質。從javascript去了解閉包,個人認為這是本末倒置的,應該先了解純粹意義上的閉包,然後再了解javascript中的閉包機制。
基于以上情況,本文将從較為形式化的角度闡述閉包概念,當做對現有網絡的資料的一個補充。
一般來說,作為程式員我們說的閉包更多是指後者,但是如果你與我一樣同時具有一點數學背景,第一次接觸“閉包”一詞是在抽象數學中的話,那麼當剛接觸到計算機中的“閉包”時多少會産生困惑,同樣,如果你是一個純粹的程式員,那麼當在數學著作中讀到“閉包”一詞時請小心區分這個“閉包”具體是表述哪一個概念。
我會在下文中分别闡述數學領域和計算機科學領域閉包的概念。
抽象代數是一門研究代數結構的數學分支,它的研究對象包括群、環、域和向量空間等等。當然我在這裡絲毫沒有要大談特談這些令人頭大的概念的意思,我會盡量以一種易懂的半形式化方式去闡述一些概念。
非正式地,集合是n個對象組成的一個無序、互異且确定的整體。其中n是自然數。這些對象稱為集合的元素。
無序性是指集合中的元素不存在序關系(集合上可以定義序關系,但這些序關系不是集合本身的一部分),每個元素的地位是相同的。
互異性是指集合中任意兩個元素是不同的,即同一集合中任意兩個元素間不存在等價關系。
确定是指對任意一個對象和任意一個集合,這個對象要麼屬于此集合,要麼不屬于此集合,二者必居其一,不存在模棱兩可的狀态(模糊數學中有一種中間隸屬狀态,本文不考慮模糊數學領域)。
非正式地,集合上的n元運算是一個映射,這個映射将作用于任意n個集合中的元素,并映射為一個結果(注意結果不一定屬于這個集合)。
例如,設
是正整數集合,那麼加法(
)和減法(
)都可以看做定義在
上的二進制運算,因為任意兩個正整數都可以進行加減。
有了集合和運算的概念,就可以定義封閉的概念了。
非正式地,如果定義于集合
上的運算
的運算結果仍然屬于
,那麼運算
對于集合
是封閉的。下面給出“封閉”的一個半形式化定義:
如果對于任意
,有
,那麼說二進制運算
對于集合a是封閉的。
例如“
”對于
是封閉的,因為任意兩個正整數的和結果仍然是正整數;但是“
不是封閉的,例如3和5屬于
,但是:
。
一個集合滿足閉包性質當且僅當這個集合在某個運算或某些運算的搜集下是封閉的,其中“某些運算的搜集下封閉”是指這個集合單獨閉合在每個運算之下。
值得一提的是,之前這條定義往往被作為一條公理引入一個代數結構,叫做“閉包公理”。但是現代集合論往往将運算形式化的定義為集合間的運算,是以将其作為公理引入代數結構是多餘的(因為可以通過其它公理間接定義閉包公理),但是對于子集是否閉合的問題,閉包公理仍然有意義。
上面說了很多東西,我們來看一個抽象代數領域閉包的例子。
我們回想在形式語言與自動機理論中(或者編譯原理中也有提到)在定義語言時做的一些推導。
一般我們會先定義一個有限集合
,叫做字母表,
的n階幂運算定義為形成一個新的集合
,一個對象屬于
當且僅當它是
中任意n個字母連接配接成的串,其中
。而
此時集合
滿足閉包性質,因為這個集合對于幂運算是封閉的。有興趣的讀者可以自行證明一下。
在這一章節開始之前,我需要再和大家明确一個比較糾結的事實,就是在函數式程式設計領域中當說到“閉包”時,也有可能是指數學領域中閉包的概念,這是因為函數式程式設計在基礎理論與抽象代數有一定親緣性,是以當在函數式語言著作中讨論“閉包”時,有可能是在抽象數學的上下文中讨論的。然而,在表述上可能會有微妙變化。在函數式語言領域對于數學閉包常用的表述是“如果一個運算的結果仍然能被此運算作用,則這個運算是封閉的”,要注意這隻不過是上文提到的“閉包”概念的另一種等價表述而已,如果我們将這個運算的所有結果看做一個集合,那麼就可以等價表述說這個運算在這個集合上是封閉的。
而我下面所要闡述的閉包是一種截然不同的概念。是以,當在函數式語言的著作中看到“閉包”時,需要根據上下文環境小心區分其表述哪種概念。
簡單來說lambda演算将計算過程看過一系列的函數代換,例如,下面是加運算的lambda函數(假設+運算已經定義):
lambda演算就是反複将函數應用于實際值,并用實際值代替參數,最終得出結果。例如下面是7+2的計算過程:
首先用第一個參數(7)代替最外層函數的參數(x),然後用第二個參數(2)代替第二層函數的參數(y),最終得到計算結果。
下面是用scheme程式對上述lambda演算的等價表示:
可以這樣計算7+2:
下面看一個稍微複雜點的例子:
這裡定義了函數f,接受一個參數x,特别要注意它的傳回值:不是一個值而是一個匿名函數。如果我們把這個函數單獨拿出來:
可以看到,這個匿名函數接收一個參數y,但是卻沒有參數x!也就是說,如果脫離上下文執行這個函數,則x處于未指定狀态,我們說對于這個函數,y是綁定的,而x是自由的。
一般地:x是一個函數的函數體中的變量,如果x被這個函數的參數指定,則x是綁定于這個函數的,否則說x對于此函數是自由的。
下面可以看到,變量的綁定和自由概念是了解閉包本質的一把鑰匙。
繼續上面的scheme程式,我們已經定義了函數f:
如果我們運作下面程式:
可以看到,f傳回了一個過程(匿名函數),按照函數演算規則,這個函數應該是:
那麼下面的運算就很直覺了:
注意這裡有一個非常重要的地方(也是閉包性質的關鍵),那就是這個運算執行了兩個函數:f和匿名函數。而f的作用域為(f 7),這就是說,其實在(f 7)之後,f這個函數就結束了,而x(這裡被指派為7)是f的私有變量(綁定于f),那麼程式設計語言的設計者就有兩種選擇:第一,在函數超出其作用域後立即銷毀其綁定變量,如果是這樣的話,((f 7) 2) 是無法得出結果的,因為在外層的f運算結束後,存放數值“7”的變量就被釋放了,是以匿名函數無法得到其自由變量x的值;顯然scheme的設計者做了第二種選擇:如果一個函數傳回另一個函數,而被傳回函數又需要外層函數的變量時,不會立即釋放這個變量,而是允許被傳回的函數引用這些變量。支援這種機制的語言稱為支援閉包機制,而這個内部函數連同其自由變量就形成了一個閉包。
上面的這段話不太好了解,但是請務必多讀幾遍,因為,這就是閉包的全部。
好的,現在開始讨論javascript中的閉包。
上文說過,閉包是函數式語言才有的機制,或者說支援函數式程式設計泛型的語言才有的性質。一個語言支援函數式程式設計泛型,如果它同時具有下列特性:
可以将一個函數指派給一個變量。
函數可以作為參數傳遞給另一個函數。
函數的傳回值可以是一個函數。
結合上面關于閉包性質的定義,就很清楚為什麼隻有支援函數程式設計泛型的語言才可以談閉包性質。
很顯然,javascript是具有上述三條性質的,是以可以說javascript擁有函數式程式設計泛型。當然,一般我們還是習慣于用指令式的寫javascript,但是其閉包本質是一樣的。為了說明這一點,我将會首先用javascript按照函數式泛型重寫上面的scheme程式,然後轉為指令式。
上文用scheme所寫的函數f,可以用javascript等價實作如下:
1
<code>function</code> <code>f(x){</code><code>return</code> <code>function</code><code>(y) {</code><code>return</code> <code>x + y; }; }</code>
可以執行與上述scheme類似的計算(因為脫離了浏覽器,我是用nodejs執行這段javascript):
2
<code>console.log( (f(7)) (2) );</code>
<code>//9</code>
其中f傳回的匿名函數與其自由變量x組成了一個閉包系統。
如果用指令式重寫上面的程式,就得到了我們熟悉的閉包:
3
4
5
6
7
8
<code>function</code> <code>f(x){</code>
<code> </code><code>return</code> <code>function</code><code>(y) {</code>
<code> </code><code>return</code> <code>x + y;</code>
<code> </code><code>};</code>
<code>}</code>
<code>var</code> <code>lam = f(7);</code>
<code>console.log(lam(2));</code>
本文分别讨論了抽象代數和函數式程式設計中兩個截然不同的閉包概念,當然,作為程式員我們更關心的是後者(但前者也不是對程式員一點用也沒有,如果學習函數式語言的構造原理,抽象代數中的閉包也是必須了解的概念)。實際上,閉包遠沒有很多人想象的複雜和神秘,隻不過是函數式程式設計中一個普通的概念,但是可能因為我們大多數人是從指令式程式設計語言學習程式設計,很少去寫函數式程式,而要了解閉包卻是需要結合函數式程式設計泛型,是以很難看清閉包的本質。希望本文能對您有所幫助,同時我個人也建議可以學習一門函數式語言,這樣對很多概念的了解非常有好處。
原貼位址:http://www.codinglabs.org/html/closure-perspective-of-abstract-mathematic-and-functional-language.html