通過示例學習JavaScript閉包

  • 2019 年 12 月 31 日
  • 筆記

譯者按: 在上一篇部落格,我們通過實現一個計數器,了解了如何使用閉包(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"

總結

嚴格來講,我對閉包的解釋並不準確。不過,將閉包簡單地看做局部變數,理解起來會更加簡單。

參考鏈接

版權聲明

轉載時請註明作者 Fundebug以及本文地址: https://blog.fundebug.com/2017/08/07/javascript-closure-examples/

Tags: