天天看點

通過示例學習JavaScript閉包

譯者按: 在 上一篇部落格

,我們通過實作一個計數器,了解了如何使用閉包(Closure),這篇部落格将提供一些代碼示例,幫助大家了解閉包。

原文:

JavaScript Closures for Dummies 譯者: Fundebug 為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用于學習。

閉包并不神奇

其實,隻要你領會了閉包的關鍵概念,一切就非常簡單了。作為JavaScript開發者,你應該可以了解以下代碼:

Example 1

function sayHello(name) 
{

  var text = 'Hello ' + name;

  var sayAlert = function() { console.log(text); }

  sayAlert();
}

sayHello("Bob") // 輸出"Hello Bob"
           

在sayHello()函數中定義并調用了sayAlert()函數;sayAlert()作為内層函數,可以通路外層函數sayHello()中的text變量。了解這一點,你就可以繼續閱讀這篇部落格了。

一個閉包示例

兩句話總結閉包(注意,這個定義并不規範,但是有助于了解):

  • 閉包就是函數的局部變量,這些變量在函數return之後仍然可以通路
  • 閉包就是函數的記憶體堆棧,這個記憶體堆棧在函數return之後并沒有被收回

Example 2

function sayHello2(name) 
{
  var text = 'Hello ' + name; // 局部變量

  var sayAlert = function() { console.log(text); }

  return sayAlert;
}

var say2 = sayHello2("Jane");
say2(); // 輸出"Hello Jane"
           

調用sayHello2()函數傳回了sayAlert,它是一個引用變量,指向一個函數。相信大多數JavaScript程式員能夠了解什麼是引用變量,而C程式員則可以把sayAlert以及say2了解為指向函數的指針。

C指針與JavaScript引用變量并無實質區分。在JavaScript中,不妨這樣了解,指向函數的引用變量不僅指向函數本身,還隐含地指向了一個閉包。

代碼中匿名函數function() { alert(text); }是在另一個函數,即sayHello2()中定義的。在JavaScript中,如果你在函數中定義了一個函數,則建立了閉包。

對于C語言,以及其他絕大多數語言:函數return之後,其局部變量将無法通路,因為記憶體中的堆棧會被銷毀。

對于JavaScript,如果你在函數中定義函數的話,當外層函數return之後,其局部變量仍然可以通路。代碼中已經證明了這一點:當sayHello2()函數return之後,我們調用了say2()函數,成功列印了text變量,而text變量正是sayHello2()函數的局部變量。

更多示例

如果隻是從定義的角度去了解閉包,顯然是非常困難。然而,如果通過代碼示例去了解閉包,則簡單很多。是以,強烈建議你認真地了解每一個示例,弄清楚它們是如何運作的,這樣你會避免很多奇怪的BUG。

Example 3

Example 3中,say667()函數return後,num變量将仍然保留在記憶體中。并且,sayNumba函數中的num變量并非複制而是引用,是以它輸出的是667而非666。

function say667() {

  var num = 666; // say667()函數return後,num變量将仍然保留在記憶體中

  var sayAlert = function() { console.log(num); }

  num++;

  return sayAlert;

}

var sayNumba = say667();

sayNumba(); // 輸出667
           

Example 4

Example 4中,3個全局函數gAlertNumber,gIncreaseNumber,gSetNumber指向了同一個閉包,因為它們是在同一次setupSomeGlobals()調用中聲明的。它們所指向的閉包就是setupSomeGlobals()函數的局部變量,包括了num變量。也就是說,它們操作的是同一個num變量。

function setupSomeGlobals() {

  var num = 666;

  gAlertNumber = function() { console.log(num); }

  gIncreaseNumber = function() { num++; }

  gSetNumber = function(x) { num = x; }

}

setupSomeGlobals();
gAlertNumber(); // 輸出666

gIncreaseNumber();
gAlertNumber(); // 輸出667

gSetNumber(5);
gAlertNumber(); // 輸出5
           

Example 5

Example 5的代碼比較難,不少人都會犯同樣的錯誤,因為它的執行結果很可能違背了你的直覺。

function buildList(list) 
{
  var result = [];

  for (var i = 0; i < list.length; i++) 
  {
    var item = 'item' + list[i];
    result.push( function() { console.log(item + ' ' + list[i])} );
  }

  return result;
}

var fnlist = buildList([1,2,3]);

for (var j = 0; j < fnlist.length; j++) 
{
  fnlist[j](); // 連續輸出3個"item3 undefined"
}
           

result.push( function() {alert(item + ' ' + list[i])}将指向匿名函數function() {alert(item + ' ' + list[i])}的引用變量加入了數組,其效果等價于:

pointer = function() {alert(item + ' ' + list[i])};
result.push(pointer);
           

代碼執行後,連續輸出了3個"item3 undefined",明顯與直覺不同。

調用buildList()函數之後,我們得到了一個數組,數組中有3個函數,而這3個函數指向了同一個閉包。而閉包中的item變量值為"item3",i變量值為3。如果了解了3個函數指向的是同一個閉包,則輸出結果就不難了解了。

Example 6

Example 6中,alice變量在sayAlert函數之後定義,這并未影響代碼執行。因為傳回函數sayAlice2所指向的閉包會包含sayAlice()函數中的所有局部變量,這自然包括了alice變量,是以可以正常列印"Hello Alice"。

function sayAlice() 
{
  var sayAlert = function() { console.log(alice); }

  var alice = 'Hello Alice';

  return sayAlert;
}

var sayAlice2 = sayAlice();

sayAlice2(); // 輸出"Hello Alice"
           

Example 7

由Example 7可知,每次調用newClosure()都會建立獨立的閉包,它們的局部變量num與ref的值并不相同。

function newClosure(someNum, someRef) 
{
  var anArray = [1,2,3];
  var num = someNum;
  var ref = someRef;

  return function(x) 
  {
      num += x;

      anArray.push(num);

      console.log('num: ' + num + "; " + 'anArray ' + anArray.toString() + "; " + 'ref.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 closure 1"
closure2(-10); // 列印"num: 990; anArray 1,2,3,990; ref.someVar closure 2"
           

總結

嚴格來講,我對閉包的解釋并不準确。不過,将閉包簡單地看做局部變量,了解起來會更加簡單。

參考連結

通過示例學習JavaScript閉包

版權聲明:

轉載時請注明作者Fundebug以及本文位址:

https://blog.fundebug.com/2017/08/07/javascript-closure-examples/

繼續閱讀