JavaScript閉包模型

                                                                            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 }
View Code

 


 

      《二》 閉包實例編程語言

  兩句話總結:ide

  • 閉包是支持 頭等函數(first-class functions)的一種函數;它是一個能在其做用域類引用變量(變量先被聲明),賦值給另一個變量,做爲一個參數傳遞給其餘函數,或是做爲一個函數的結果返回。又或者--
  • 閉包是函數開始執行時分配出來的一個棧幀,且當函數返回後不會被釋放(相似於‘棧幀’是在堆上被分配而不是在棧上被分配!)。

下面的代碼返回給另外一個函數一個引用。函數式編程

       

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"
View Code

   運行結果:(--PS:譯者添加--)函數

       

     大部分的開發者都夠能理解上面的代碼中一個函數的引用是如何返回給一個變量(say2)。若是你這還不能看明白的話,那麼你須要去學習基礎的知識而不該該是閉包了。一個C 語言開發者會理解爲一個函數返回了一個指針給另一個函數,變量saysay2分別是兩個函數的指針。

     而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:

本例演示了局部變量不被複制—它們被保存在引用中。這就好像當存在着一個函數的時候在內存中存一個相應的棧幀。

        

View Code

實例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
View Code

實例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
View Code

實例3運行結果:(--PS:譯者添加--)

 這一行代碼:result.push(function() {console.log(item + ' ' + list[i])} ;result數組對匿名函數加引用了三次。若是不是很理解匿名函數能夠思考下面的例子:

1 pointer = function() {console.log(item + ' ' + list[i])};
2 result.push(pointer);
View Code

注意到當運行這段代碼的時候,結果輸出三次"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"
View Code

實例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;
View Code

實例4運行結果:(--PS:譯者添加--)

總結:

     若是看到這裏仍是以爲不夠清晰,那最好的方式就是把每個例子實現一遍,閱讀一段文檔相比於理解實例困難太多了。關於閉包和棧幀的說明,不能保證絕對技術無誤——都有簡化以助於理解。

概括以下幾點:

  • 只要在一個函數裏面使用其餘函數,那麼閉包就被使用了。
  • 只要在函數中使用了eval()函數,那麼閉包就被使用了,eval能引用該函數的局部變量,且在eval中甚至還可使用eval('var foo = ...' )建立新的局部變量。
  •  只要在一個函數中使用了 new Function(...)(函數構造子),那麼就不會建立閉包。(new出來的函數不能引用外部函數的局部變量)
  • JavaScript中的閉包至關於保存全部局部變量的一個拷貝,當一個函數退出,就變成它本身自己。
  • 可能這樣想是最好的,認爲閉包老是在一個函數的入口被建立,它的局部變量被加到閉包中。
  • 每一次含有閉包的函數被調用,那麼局部變量的集合就會被保存一次。(給定函數裏面包含函數的聲明,且對裏面的函數存在一個引用被返回或是外部的引用也以某種形式保存)。
  • 兩個函數就好像他們有相同的源文本,卻因‘隱形’的閉包表現得徹底不同。我認爲JavaScript代碼不能絕對地找出是否一個函數引用存在一個閉包。
  • 若是你試圖作出一些動態源碼的改變(例如:myFunction = Function(myFunction.toString().replace(/Hello/.'Hola'));),若是myFunction是閉包的話,那麼它就不會有效果。
  • 有可能在函數裏面聲明的函數中獲取函數的聲明。且可以在得到超過一層的閉包。
  • JavaScript中的閉包跟其餘函數式編程語言有明顯差別。

 

 

連接:

感謝:

 若是你學習過閉包(在這或是其餘任何地方),不管是任何形式的改變的提議的反饋,只要能使本文變得更加清晰明瞭,我都很是感興趣。

 

 

 

(翻譯自stackoverflow社區;原文信息:做者:Morris  發佈時間:2006-02-21)

原文連接

相關文章
相關標籤/搜索