譯者按: 在上一篇博客,我們通過實現一個計數器,了解了如何使用閉包(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
Fundebug專注於JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了7億+錯誤事件,得到了Google、360、金山軟件、百姓網等眾多知名用戶的認可。歡迎免費試用!
版權聲明:
轉載時請注明作者Fundebug以及本文地址: https://blog.fundebug.com/2017/08/07/javascript-closure-examples/