JavaScript閉包模型javascript
----- [原創翻譯]2016-09-01 09:32:22 html
《 一》 閉包並不神祕java
本文利用JavaScript代碼來闡述閉包,目的是爲了使普通開發者能很好的理解閉包,並不面向專家或函數式編程開發者。編程
一旦領悟了閉包的核心思想,它就再也不難於理解;然而,只看一些理論上的文檔或是以理論爲中心的文檔只會讓你南轅北轍。數組
本文適用於那些在主流編程語言中有必定編程經歷的開發者們,且能看懂以下的JavaScript函數:閉包
1 function sayHello(name) { 2 var text = 'Hello ' + name; 3 var say = function() { console.log(text); } 4 say(); 5 }
《二》 閉包實例編程語言
兩句話總結:ide
下面的代碼返回給另外一個函數一個引用。函數式編程
1 function sayHello2(name) { 2 var text = 'Hello ' + name; // Local variable 3 var say = function() { console.log(text); } 4 return say; 5 } 6 var say2 = sayHello2('Bob'); 7 say2(); // logs "Hello Bob"
運行結果:(--PS:譯者添加--)函數
大部分的開發者都夠能理解上面的代碼中一個函數的引用是如何返回給一個變量(say2)。若是你這還不能看明白的話,那麼你須要去學習基礎的知識而不該該是閉包了。一個C 語言開發者會理解爲一個函數返回了一個指針給另一個函數,變量say 與say2分別是兩個函數的指針。
而C語言中指針指向一個函數與JavaScript裏一個函數的引用有一個很關鍵的區別。那就是,在JavaScript中,你能夠理解爲一個函數引用變量是一個指針指向一個函數,同時又是一個隱式的指針指向閉包。
上面的代碼中存在一個閉包,由於匿名函數 function() {console.log(text);}在其餘函數裏面有被定義了,實例中在sayHeelo2()函數中定義的。在JavaScript中,若是你在另外一個函數中使用function關鍵字,那麼你就正在建立一個閉包。
在C與其餘相似的編程言語裏,當一個函數返回後,全部的局部變量將不會再可利用,由於棧幀已經被銷燬。
在JavaScript中,若是你在一個函數中聲明另外一個函數,那麼局部變量在從你調用的函數返回後依然可使用。正像上面演示的那樣,由於咱們從sayHello2()返回以後再調用的say2()注意到咱們調用的代碼裏面有text這個變量,而這個變量是sayHello2()函數裏的一個局部變量。
仔細觀察say2.toString()的輸出,咱們不難發現這段代碼引用了text變量。匿名函數可以引用包含值爲'Hello Bob'的text,緣由就在於sayHello2()的局部變量被保存在閉包中。
神祕就在於JavaScript裏的函數引用也是對函數中建立的閉包的一個隱式引用---相似於委託是加在某一個對象上面的隱式引用的一個方法指針。
《三》 更多實例
出於某些緣由,當你只看相關文檔時,發現閉包好像確實難於理解,可是當你結合一些實例就可以瞭解到它們究竟是如何工做的(可能會花費咱們一些時間)。建議在實踐中認真學習直到你理解它們是如何工做的。若是你在沒有徹底理解閉包是如何工做的就使用她們,估計你可能搞出不少很是糟糕的bug。
實例1:
本例演示了局部變量不被複制—它們被保存在引用中。這就好像當存在着一個函數的時候在內存中存一個相應的棧幀。
實例1運行結果:(--PS:譯者添加--)
實例2:
三個全局函數都是對同一個閉包的引用,由於它們都被定義成單獨調用setupSomeGlobals()的函數。
1 var gLogNumber, gIncreaseNumber, gSetNumber; 2 function setupSomeGlobals() { 3 // Local variable that ends up within closure 4 var num = 42; 5 // Store some references to functions as global variables 6 gLogNumber = function() { console.log(num); } 7 gIncreaseNumber = function() { num++; } 8 gSetNumber = function(x) { num = x; } 9 } 10 11 setupSomeGlobals(); 12 gIncreaseNumber(); 13 gLogNumber(); // 43 14 gSetNumber(5); 15 gLogNumber(); // 5 16 17 var oldLog = gLogNumber; 18 19 setupSomeGlobals(); 20 gLogNumber(); // 42 21 22 oldLog() // 5
實例2運行結果:(--PS:譯者添加--)
當三個函數被定義的時候,它們共享同一個閉包的入口——setupSomeGlobals()函數的局部變量。
在上面的例子中,若是再次調用setuoSomeGlobals()函數,那麼一個全新的閉包(棧幀!)將建立。原來的三個變量:gLoNumber, gIncreaseNumberm fSetNumber 將被產生新的閉包的函數重寫;(在JavaScript中,不管什麼時候你在一個函數裏面聲明瞭另外一個函數,當外部的函數被調用的時候裏面的函數都會被再建立一次。)
實例3:
下面的實例對開發者來講並不陌生,所以須要很好地理解一下。當在一個循環中定義一個函數的時候要特別地當心:閉包裏的局部變量可能並不會像你一開始想象那樣執行。
1 function buildList(list) { 2 var result = []; 3 for (var i = 0; i < list.length; i++) { 4 var item = 'item' + i; 5 result.push( function() {console.log(item + ' ' + list[i])} ); 6 } 7 return result; 8 } 9 10 function testList() { 11 var fnlist = buildList([1,2,3]); 12 // Using j only to help prevent confusion -- could use i. 13 for (var j = 0; j < fnlist.length; j++) { 14 fnlist[j](); 15 } 16 } 17 18 testList() //logs "item2 undefined" 3 times
實例3運行結果:(--PS:譯者添加--)
這一行代碼:result.push(function() {console.log(item + ' ' + list[i])} ;result數組對匿名函數加引用了三次。若是不是很理解匿名函數能夠思考下面的例子:
1 pointer = function() {console.log(item + ' ' + list[i])}; 2 result.push(pointer);
注意到當運行這段代碼的時候,結果輸出三次"item2 undefined"!這是由於就如前面的例子所示,bulidList中的局部變量只存在一個閉包。當fnlist[j]調用匿名函數的時候;他們共用一個閉包,且在閉包中使用i與item的當前值(這時循環結束 i 的值爲3,item的值爲‘item2’)。下標從0開始的,因此是item2,而i++將使得i的值爲3。
實例4:
本實例演示了閉包包含任何在其終止以前函數內聲明的局部變量,變量alice 在匿名函數以後被聲明。匿名函數首先被聲明,當它被調用的時候,他能夠傳遞alice變量,由於alice變量是在其相同的做用域內(JavaScript容許變量掛起),sayAlice()()直接調用函數引用並從sayAlice()返回—其正好如以前作的同樣,只是沒有臨時變量。
1 function sayAlice() { 2 var say = function() { console.log(alice); } 3 // Local variable that ends up within closure 4 var alice = 'Hello Alice'; 5 return say; 6 } 7 sayAlice()();// logs "Hello Alice"
實例4運行結果:(--PS:譯者添加--)
巧妙性:注意到變量say也在閉包之中,其能夠被任何函數在其sayAlice()函數中存取,或者能夠在函數內被遞歸地存取。
實例4:
最後一個實例代表:局部變量每一次調用都會建立一個獨立的閉包,在每個函數聲明中都沒有單一的閉包存在。下面是每一次調用函數的閉包。
1 function newClosure(someNum, someRef) { 2 // Local variables that end up within closure 3 var num = someNum; 4 var anArray = [1,2,3]; 5 var ref = someRef; 6 return function(x) { 7 num += x; 8 anArray.push(num); 9 console.log('num: ' + num + 10 '\nanArray ' + anArray.toString() + 11 '\nref.someVar ' + ref.someVar); 12 } 13 } 14 obj = {someVar: 4}; 15 fn1 = newClosure(4, obj); 16 fn2 = newClosure(5, obj); 17 fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4; 18 fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4; 19 obj.someVar++; 20 fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5; 21 fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
實例4運行結果:(--PS:譯者添加--)
總結:
若是看到這裏仍是以爲不夠清晰,那最好的方式就是把每個例子實現一遍,閱讀一段文檔相比於理解實例困難太多了。關於閉包和棧幀的說明,不能保證絕對技術無誤——都有簡化以助於理解。
概括以下幾點:
連接:
感謝:
若是你學習過閉包(在這或是其餘任何地方),不管是任何形式的改變的提議的反饋,只要能使本文變得更加清晰明瞭,我都很是感興趣。
(翻譯自stackoverflow社區;原文信息:做者:Morris 發佈時間:2006-02-21)