什麼是JavaScript閉包終極全解之一——基礎概念

Richaaaard發表於2015-08-24

什麼是JavaScript閉包終極全解之一——基礎概念

“閉包是JavaScript的一大謎團。最近的一項調查顯示,有關JavaScript的閉包的部落格文章佔23%左右” [1]

引子

在阮一峰博士的部落格中[2],已經對JavaScript的閉包概念解釋得非常詳細,但是博主還是覺得有必要,對閉包這一名詞以JavaScript為例,從概念到應用做更為深入研究,方便讀者更為透徹的理解。

 

首先借用阮老師對閉包(closure)的概念做出的定義(或描述):

 

阮老師的理解是:“閉包就是能夠讀取其他函式內部變數的函式”。並且認為:“可以把閉包簡單理解成‘定義在一個函式內部的函式’”。暫且先不評論這種定義是否合適、是否正確?(讀者請注意,這裡我沒有說阮老師說的不對)

我們先來看看另一個對於閉包概念的相關描述。

在《JavaScript高階程式設計(第3版)》中文版中[3],具體描述在第7章函式表示式第7.2節(頁碼為第178頁):閉包是指有權訪問另一個函式作用域中的變數的函式。建立閉包的常見方式,就是在一個函式內部建立另一個函式。

先拋開對於兩個出處的具體描述,至少兩者對於閉包的定義可以簡化為: “閉包是一種函式”,而且閉包是一種特殊的函式。以上兩個出處涉及到了一些概念(或名詞):函式、內部變數、函式內部、函式作用域。本文會對這些概念做出適度的相關解釋。

概念

程式設計中的Closure

計算機程式設計(Computer Programming)中對於閉包(Closure)的定義是我們對於JavaScript閉包(Closure)理解的本質基礎。 

In programming languages, closures (also lexical closures or function closures) are a technique for implementing lexically scoped name binding in languages with first-class functions.Operationally, a closure is a record storing a function together with an environment:a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or storage location to which the name was bound when the closure was created. A closure—unlike a plain function—allows the function to access those captured variables through the closure's reference to them, even when the function is invoked outside their scope. [4]

 

釋義是:

在程式設計語言中[4],閉包(即詞法閉包lexical closures或函式閉包function closures)是用一等函式[5](first class)實現詞法作用域(lexically scoped)名稱繫結[6](name binding)的一種技術。操作上說,一個閉包是一個用來儲存一個函式及其環境的一條記錄[7](record),這個環境會在閉包建立的時候,在函式的每個自由變數[8](free variable——在區域性使用,但是定義在外部作用域)與其值或名稱繫結的儲存區之間建立一個對映。不同於一般普通函式(plain function),即使當函式在其作用域外被呼叫時,閉包仍然允許函式通過閉包的引用訪問(存取)捕獲的變數(captured variables)。

 

以上釋義比較抽象,我們先用JavaScript語言舉一個簡單的例子並加以說明(我在此採用維基百科中說給出的例子並加以改寫),在段程式碼與以上概念之間建立一個對映,讓大家有一個直觀的概念。稍後,我們再來理解這些概念是什麼、如何理解?

 

 1 function   startAt(CAPTURED) {
 2 
 3 function incrementBy(i) {
 4 
 5       return CAPTURED + i;
 6 
 7 }
 8 
 9 return incrementBy;
10 
11 }

以上的程式碼片段定義了一個高階函式(higher-order function)startAt,這個函式接收一個引數CAPTURED(我將由閉包捕獲的變數大寫。這並不是JavaScript的標準實踐,也不鼓勵這樣做,這裡只是為了方便說明[4])和一個內部函式(nested function) incrementBy。儘管CAPTURE變數不是incrementBy的區域性變數,由於incrementBy處於CAPTURED變數的詞法作用域中,這個內部函式可以訪問變數CAPTURED。函式startAt返回了一個包含函式incrementBy的閉包,這個函式把變數i與CAPTURED相加然後和CAPTURED在當前(this)呼叫startAt的一個引用返回,所以當incrementBy被呼叫時會知道當前的CAPTURED的值。

應用以上函式得到執行結果如下:

 1 var   startAt10 = startAt(10); // closure1
 2 
 3 var   startAt100 = startAt(100); // closure2
 4 
 5 startAt10(1);
 6 
 7 >>   11
 8 
 9 startAt100(1);
10 
11 >>   101

我們注意到,因為startAt返回一個函式,所以變數startAt10 和 startAt100 是屬於函式型別。執行startAt10(1)會返回11,執行startAt100(1)會返回101。儘管startAt10和startAt100都關聯到同一個函式incrementBy,相關的環境卻不同,呼叫閉包會將CAPTURED與不同值的不同變數繫結,因此執行函式會得到不同的結果。

關於自由變數(free variable)和名稱繫結(name binding)

自由變數(free variable)

在計算機程式設計中[8],術語自由變數(free variable)是指函式中使用的變數,它們既不是函式的區域性變數也不是引數。在當前語境下,術語非區域性變數(non-local variable)是它的同義詞。 

與自由變數(free variable)對應的術語是約束變數(bound variable),約束變數在某個狀態之前是自由的,在某個狀態之後它會被繫結到一個或一組具體的值。在程式設計語言中,變數可分為自由變數與約束變數兩種。簡單來說,區域性變數和引數都被認為是約束變數;而不是約束變數的則是自由變數。[9] 

在作者翻閱的眾多JavaScript書籍中,只有Michael Fogus的《JavaScript函數語言程式設計》[1]中文版對自由變數有較為詳細的說明,具體描述在第3章變數的作用域和閉包第3.5節閉包(頁碼為第55頁),而書中對自由變數的相關說明也只是描述解釋性非定義性: 

“自由變數與閉包的關係是,自由變數閉合於閉包的建立。閉包背後的基本原理是,如果一個函式包含內部函式,那麼它們都可以看到其中宣告的變數;這些變數被稱為‘自由’變數。然而,這些變數可以被內部函式捕獲,從高階函式中return實現‘越獄’,以供以後使用。唯一需要注意的是,捕獲函式必須在外部函式內定義。函式內沒有任何區域性宣告之前(既不是被傳入,也不是區域性宣告)使用的變數就是被捕獲的變數。” 

書中給出一個例子(這個例子與維基百科中給出的例子相似): 

 1 function   makeAdder(CAPTURED) {
 2 
 3 return function(free) {
 4 
 5       return  free + CAPTURED
 6 
 7 };
 8 
 9 }
10 
11 var   add10 = makeAdder(10);
12 
13 add10(32);
14 
15 //=>   42
16 
17  
18 
19 var   add1024 = makeAdder(1024);
20 
21 add1024(11);
22 
23 //=>   1035
24 
25  
26 
27 add10(98);
28 
29 //=>   108

如何對應例子理解上面一段話的內容? 

“如果一個函式包含內部函式,那麼它們都可以看到其中宣告的變數;這些變數被稱為‘自由’變數。”

——以上makeAdder函式包含了一個匿名的內部函式用以將free 和 CAPTURED兩個變數相加。這裡“它們都可以看到”中的“它們”,在語境下應該指示內部函式,內部匿名函式可以看到CAPTURED這個變數,因此CAPTURED變數被稱為“自由”變數。 

這裡free是不是自由變數呢?

——不是。這也是閱讀時容易給讀者造成誤解的地方。顯然,維基百科中的定義:“自由變數(free variable)是指函式中使用的變數,它們既不是函式的區域性變數也不是引數”。而此處,free是內部匿名函式的引數,顯然不是自由變數。所以《JavaScript函數語言程式設計》[1]中給出的例子,還是應該理解為原作者給出的一個“免費”的意思,而非譯註上面理解的“自由”變數。比較好的寫法是在此語境下將free命名成別的名字,以免混淆。這也是我在改寫維基百科中的例子時,採用了CAPTURED的寫法而拋棄了free命名。 

在《JavaScript函數語言程式設計》[1]中:“唯一需要注意的是,捕獲函式必須在外部函式內定義。函式內沒有任何區域性宣告之前(既不是被傳入,也不是區域性宣告)使用的變數就是被捕獲的變數。”

在維基百科Closure[4]中:“不同於一般普通函式(plain function),即使當函式在其作用域外被呼叫時,閉包仍然允許函式通過閉包的引用訪問(存取)捕獲的變數(captured variables)。” 

因此,簡單來理解“捕獲的變數”和“自由變數”只是同一個變數分別在動態作用域(執行時)和詞法作用域(靜態時)的兩種不同名稱。

名稱繫結(name binding)

在計算機程式語言中[6],名稱繫結就是將實體(entities- data and/code)與識別符號(identifiers)相關聯的過程。我們將一個識別符號繫結到一個物件上叫作引用(reference)該物件。機器語言沒有內建的識別符號記法(notion),但是名稱與物件(name-object)繫結做為一個服務和記法(notation)被程式語言實現並且被程式設計師所使用。繫結與作用域的概念緊密相關,因為作用域從詞法上,在計算機編碼中定義了何名稱在何種可能執行的路徑下於何處繫結何具體物件。

相關名稱繫結概念的具體分支在此篇中不做展開,會在後續系列中詳細說明。

結語

在Mozilla的相關開發文件中[10],我們可以看到對於Closure的定義為:“Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.”。即“閉包是使用獨立(自由)變數的函式,換句話說,閉包定義的函式記錄了它被建立時的環境。

 

總而言之,關於JavaScript閉包我做出以下理解(限於計算機程式設計語境下):

簡單理解(基本)

閉包是一個函式

細化(來自阮一峰部落格)

閉包是定義在一個函式內部,並且能夠讀取其他函式內部變數的函式

明確(來自《JavaScript高階程式設計》第三版)

閉包是定義在一個外部函式內部,並且能夠訪問(存取)外部函式中自由變數的函式

廣義抽象(來自Mozilla與維基百科)

閉包是一個抽象的環境記錄,它包含狹義的閉包函式以及在建立該函式時,每個自由變數及其值或名稱繫結儲存區直接的一個對映。

 

參考

[1] Michael Fogus著 歐陽繼超 王妮譯 JavaScript函數語言程式設計[M]. 人民郵電出版社,2015: 44-60

[2] http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

[3] Nicholas C.Zakas著 李鬆峰 曹力譯 JavaScript高階程式設計(第3版)[M]. 人民郵電出版社,2012: 178-182

[4] https://en.wikipedia.org/wiki/Closure_(computer_programming)

[5] https://en.wikipedia.org/wiki/First-class_function

[6] https://en.wikipedia.org/wiki/Name_binding

[7] https://en.wikipedia.org/wiki/Record_(computer_science)

[8] https://en.wikipedia.org/wiki/Free_variables_and_bound_variables

[9] http://blog.jobbole.com/47296/

[10] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

[11] David Herman著 黃博文 喻楊譯 Effective JavaScript編寫高質量JavaScript程式碼的68個有效方法. 機械工業出版社,2014: 31-34

 

相關文章