理解Javascript的閉包
前言:還是一篇入門文章。Javascript 中有幾個非常重要的語言特性——對象、原型繼承、閉包。其中閉包對于那些使用傳統靜態語言C/C++的程序員來說是一個新的語言特性。本文將以例子入手來 介紹 Javascript 閉包的語言特性,并結合一點 ECMAScript 語言規范來使讀者可以更深入的理解閉包。
注:本文是入門文章,例子素材整理于網絡,如果你是高手,歡迎針對文章提出技術性建議和意見。本文討論的是 Javascript,不想做語言對比,如果您對 Javascript 天生不適,請自行繞道。
什么是閉包
閉包是什么?閉包是 Closure,這是靜態語言所不具有的一個新特性。但是閉包也不是什么復雜到不可理解的東西,簡而言之,閉包就是:
- 閉包就是函數的局部變量集合,只是這些局部變量在函數返回后會繼續存在。
- 閉包就是就是函數的“堆棧”在函數返回后并不釋放,我們也可以理解為這些函數堆棧并不在棧上分配而是在堆上分配
- 當在一個函數內定義另外一個函數就會產生閉包
上面的第二定義是第一個補充說明,抽取第一個定義的主謂賓——閉包是函數的‘局部變量’集合。只是這個局部變量是可以在函數返回后被訪問。(這個不是官方定義,但是這個定義應該更有利于你理解閉包)
做為局部變量都可以被函數內的代碼訪問,這個和靜態語言是沒有差別。閉包的差別在于局部變變量可以在函數執行結束后仍然被函數外的代碼訪問。這 意味著函數必須返回一個指向閉包的“引用”,或將這個”引用”賦值給某個外部變量,才能保證閉包中局部變量被外部代碼訪問。當然包含這個引用的實體應該是 一個對象,因為在 Javascript 中除了基本類型剩下的就都是對象了。可惜的是,ECMAScript 并沒有提供相關的成員和方法來訪問閉包中的局部變量。但是在 ECMAScript 中,函數對象中定義的內部函數() inner function是可以直接訪問外部函數的局部變量,通過這種機制,我們就可以以如下的方式完成對閉包的訪問了。
function greeting (name) { var text = 'Hello ' + name; // local variable return function() { alert (text); }// 每次調用時,產生閉包,并返回內部函數對象給調用者 } var sayHello=greeting ("Closure"); sayHello () // 通過閉包訪問到了局部變量 text
上述代碼的執行結果是:Hello Closure,因為 sayHello ()函數在 greeting 函數執行完畢后,仍然可以訪問到了定義在其之內的局部變量 text。
好了,這個就是傳說中閉包的效果,閉包在 Javascript 中有多種應用場景和模式,比如 Singleton,Power Constructor 等這些 Javascript 模式都離不開對閉包的使用。
ECMAScript 閉包模型
ECMAScript 到底是如何實現閉包的呢?想深入了解的親們可以獲取 ECMAScript 規范進行研究,我這里也只做一個簡單的講解,內容也是來自于網絡。
在 ECMAscript 的腳本的函數運行時,每個函數關聯都有一個執行上下文場景(Execution Context) ,這個執行上下文場景中包含三個部分
- 文法環境(The LexicalEnvironment)
- 變量環境(The VariableEnvironment)
- this 綁定
其中第三點 this 綁定與閉包無關,不在本文中討論。文法環境中用于解析函數執行過程使用到的變量標識符。我們可以將文法環境想象成一個對象,該對象包含了兩個重要組件,環 境記錄(Enviroment Recode),和外部引用(指針)。環境記錄包含包含了函數內部聲明的局部變量和參數變量,外部引用指向了外部函數對象的上下文執行場景。全局的上下文 場景中此引用值為 NULL。這樣的數據結構就構成了一個單向的鏈表,每個引用都指向外層的上下文場景。
例如上面我們例子的閉包模型應該是這樣,sayHello 函數在最下層,上層是函數 greeting,最外層是全局場景。如下圖:
因此當 sayHello 被調用的時候,sayHello 會通過上下文場景找到局部變量 text 的值,因此在屏幕的對話框中顯示出”Hello Closure”
變量環境(The VariableEnvironment)和文法環境的作用基本相似,具體的區別請參看 ECMAScript 的規范文檔。
閉包的樣列
前面的我大致了解了 Javascript 閉包是什么,閉包在 Javascript 是怎么實現的。下面我們通過針對一些例子來幫助大家更加深入的理解閉包,下面共有 5 個樣例,例子來自于 JavaScript Closures For Dummies (鏡像)。
例子1:閉包中局部變量是引用而非拷貝
function say667() { // Local variable that ends up within closure var num = 666; var sayAlert = function() { alert (num); } num++; return sayAlert; } var sayAlert = say667(); sayAlert ()
因此執行結果應該彈出的 667 而非 666。
例子2:多個函數綁定同一個閉包,因為他們定義在同一個函數內。
function setupSomeGlobals () { // Local variable that ends up within closure var num = 666; // Store some references to functions as global variables gAlertNumber = function() { alert (num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; } } setupSomeGolbals (); // 為三個全局變量賦值 gAlertNumber (); //666 gIncreaseNumber (); gAlertNumber (); // 667 gSetNumber (12);// gAlertNumber ();//12
例子3:當在一個循環中賦值函數時,這些函數將綁定同樣的閉包
function buildList (list) { var result = []; for (var i = 0; i < list.length; i++) { var item = 'item' + list[i]; result.push ( function() {alert (item + ' ' + list[i])} ); } return result; } function testList () { var fnlist = buildList ([1,2,3]); // using j only to help prevent confusion - could use i for (var j = 0; j < fnlist.length; j++) { fnlist[j](); } }
testList 的執行結果是彈出 item3 undefined 窗口三次,因為這三個函數綁定了同一個閉包,而且 item 的值為最后計算的結果,但是當i跳出循環時i值為4,所以 list[4]的結果為 undefined.
例子4:外部函數所有局部變量都在閉包內,即使這個變量聲明在內部函數定義之后。
function sayAlice () { var sayAlert = function() { alert (alice); } // Local variable that ends up within closure var alice = 'Hello Alice'; return sayAlert; } var helloAlice=sayAlice (); helloAlice ();
執行結果是彈出”Hello Alice”的窗口。即使局部變量聲明在函數 sayAlert 之后,局部變量仍然可以被訪問到。
例子5:每次函數調用的時候創建一個新的閉包
function newClosure (someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push (num); alert ('num: ' + num + '\nanArray ' + anArray.toString () + '\nref.someVar ' + ref.someVar); } } closure1=newClosure (40,{someVar:'closure 1'}); closure2=newClosure (1000,{someVar:'closure 2'}); closure1(5); // num:45 anArray[1,2,3,45] ref:'someVar closure1' closure2(-10);// num:990 anArray[1,2,3,990] ref:'someVar closure2'
閉包的應用
Singleton 單件:
var singleton = function () { var privateVariable; function privateFunction (x) { ...privateVariable... } return { firstMethod: function (a, b) { ...privateVariable... }, secondMethod: function (c) { ...privateFunction ()... } }; }();
這個單件通過閉包來實現。通過閉包完成了私有的成員和方法的封裝。匿名主函數返回一個對象。對象包含了兩個方法,方法 1 可以方法私有變量,方法 2 訪問內部私有函數。需要注意的地方是匿名主函數結束的地方的’()’,如果沒有這個’()’就不能產生單件。因為匿名函數只能返回了唯一的對象,而且不能 被其他地方調用。這個就是利用閉包產生單件的方法。
參考:
- JavaScript Closures For Dummies (鏡像) 可惜都被墻了。
- Advance Javascript (Douglas Crockford 大神的視頻,一定要看啊) 來自: coolshell.cn