Javascript閉包并非魔法

1.jpg

本文翻譯自JavaScript closures for beginners

閉包不是什么魔法

本篇文章介紹了閉包,方便程序員們能夠進(jìn)一步理解javascript代碼,本文適合有一定編程經(jīng)驗(yàn)的程序員,比如可以看懂如下代碼:大神請(qǐng)繞道。

Example 1
function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');  //Hello Joe

一旦深刻理解了核心概念,閉包就并不難分析和運(yùn)用了。

一個(gè)關(guān)于閉包的案例

兩句話總結(jié):

  • 第一級(jí)函數(shù)支持閉包。閉包它是一個(gè)表達(dá)式,可以在閉包的范圍內(nèi)引用變量(當(dāng)它被首次聲明),被賦值給變量,作為參數(shù)傳遞給函數(shù),或作為函數(shù)結(jié)果返回。(譯者注:在JavaScript世界中函數(shù)是一等公民,它不僅擁有一切傳統(tǒng)函數(shù)的使用方式(聲明和調(diào)用),還可以做到像原始值一樣賦值、傳參、返回,這樣的函數(shù)也稱之為第一級(jí)函數(shù)(First-class Function)
  • 閉包是在函數(shù)開始執(zhí)行時(shí)分配的堆棧幀,并且在函數(shù)返回后不會(huì)釋放(就像“堆棧幀”分配在堆上而不是棧上?。?/li>
Example 2

以下代碼返回一個(gè)函數(shù)的引用:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

大多數(shù)JavaScript程序員都了解如何將一個(gè)函數(shù)的引用賦給上述代碼中的變量say2。如果你不了解,那么在學(xué)習(xí)閉包之前你需要先了解一下。一個(gè)使用C的程序員會(huì)將其看作是返回指向某函數(shù)的指針,會(huì)認(rèn)為變量saysay2都是指向函數(shù)的指針。
C語言指向函數(shù)的指針和JavaScript的函數(shù)引用之間存在著很關(guān)鍵的區(qū)別。在JavaScript中,您可以將函數(shù)引用變量看作既包含指向函數(shù)的指針,也包含指向閉包的隱藏指針。

上述代碼中存在一個(gè)閉包,因?yàn)槟涿瘮?shù)function() { console.log(text); }在另一個(gè)函數(shù)sayHello2()中聲明。在這個(gè)例子中,如果你在另一個(gè)函數(shù)體里使用function關(guān)鍵字,那么你正在創(chuàng)建閉包。

在C語言和其他大多數(shù)類似語言中,在函數(shù)返回后,所有局部變量不可再被訪問,因?yàn)槎褩呀?jīng)被銷毀了。

而在Javascript語言中,如果你在一個(gè)函數(shù)體內(nèi)再聲明一個(gè)函數(shù),這個(gè)函數(shù)被返回到了全局,局部變量依然可以被訪問。如上面所示,我們?cè)诤瘮?shù)sayHello2()返回后調(diào)用了函數(shù)say2(),請(qǐng)注意,變量text是函數(shù)sayHello2()的局部變量。

function() { console.log(text); } // Output of say2.toString();

注意say2.toString()的輸出,我們可以看到這段代碼引用了變量text,由于sayHello2()的局部變量被保存到閉包內(nèi),所以這個(gè)匿名函數(shù)可以引用存儲(chǔ)"Hello Bob"的變量text

在Javascript中函數(shù)引用包含指向它所創(chuàng)建的閉包的隱藏指針就類似于js中的事件委托(一個(gè)事情本需要自己做,但自己委托給別人做了)。

更多案例

出于某種原因,閉包似乎很難理解,但是當(dāng)你多看一些案例之后,它的工作原理變得逐漸清晰(我花了很長(zhǎng)時(shí)間才搞清楚)。我建議你仔細(xì)研究這些案例,直至弄明白閉包是如何工作的。如果你在沒弄明白之前就使用閉包,就一定會(huì)碰到一些非常奇怪的錯(cuò)誤。

Example 3

這個(gè)案例表明,局部變量沒有被復(fù)制,而是它們的引用被保存。就好像當(dāng)外部函數(shù)退出后在內(nèi)存保留一個(gè)堆棧幀。

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43
Example 4

所有這三個(gè)全局函數(shù)都有一個(gè)對(duì)同一個(gè)閉包的共同引用,因?yàn)樗鼈兌际窃谕粋€(gè)setupSomeGlobals()函數(shù)中聲明的。

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

這三個(gè)函數(shù)共用一個(gè)閉包——三個(gè)函數(shù)被定義時(shí),函數(shù)setupSomeGlobals()的局部變量。
請(qǐng)注意,在上述案例中,如果再次調(diào)用setupSomeGlobals(),則會(huì)創(chuàng)建一個(gè)新的閉包(堆棧幀)。舊的gLogNumber, gIncreaseNumber, gSetNumber變量被具有新閉包的新函數(shù)覆蓋(在Javascript中,無論何時(shí)在另一個(gè)函數(shù)內(nèi)聲明一個(gè)函數(shù),每次調(diào)用外部函數(shù)時(shí)都會(huì)重新創(chuàng)建內(nèi)部函數(shù))。

Example 5

這個(gè)案例對(duì)于許多人來說是一個(gè)大難題,你需要仔細(xì)理解一下。如果你要在一個(gè)循環(huán)體中定義一個(gè)函數(shù),要非常小心,閉包中的局部變量可不會(huì)想你想當(dāng)然那樣工作。

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

function testList() {
    var fnlist = buildList([1,2,3]);
    // Using j only to help prevent confusion -- could use i.
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //logs "item2 undefined" 3 times

這行代碼result.push( function() {console.log(item + ' ' + list[i])} );所示,將一個(gè)匿名函數(shù)的引用添加到result數(shù)組中三次。如果你對(duì)匿名函數(shù)不熟悉,也可當(dāng)成如下:

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

請(qǐng)注意,當(dāng)案例執(zhí)行時(shí),"item2 undefined"會(huì)輸出三次!這是因?yàn)楦鞍咐粯樱?code>buildList的局部變量只有一個(gè)閉包。當(dāng)在執(zhí)行fnList[j]()調(diào)用匿名函數(shù)時(shí),三個(gè)匿名函數(shù)都共用一個(gè)閉包,并且它們使用的是循環(huán)結(jié)束后的當(dāng)前值作為該閉包中的iitem(循環(huán)已完成,i的值為3,item值為"item2")。請(qǐng)注意,該循環(huán)從0開始索引,到循環(huán)結(jié)束前item值為"item2",而i ++會(huì)將i值增加到3。

Example 6

此案例顯示:在外部函數(shù)退出前,外部函數(shù)內(nèi)聲明的所有全局變量都包含在閉包內(nèi)。請(qǐng)注意,變量alice實(shí)際上是在匿名函數(shù)之后聲明的,匿名函數(shù)是最先聲明的,當(dāng)該函數(shù)被調(diào)用時(shí),它仍然可以訪問alice變量,因?yàn)樵撟兞刻幱谙嗤饔糜騼?nèi)(Javascript聲明提升)。另外,sayAlice()()只是直接調(diào)用從sayAlice()返回的函數(shù)引用。

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

需要注意的是:say變量也在閉包中,可以通過sayAlice()中任何可能聲明的其他函數(shù)訪問,或者可以在內(nèi)部函數(shù)內(nèi)遞歸訪問。

Example 7

最后這個(gè)案例表明,每次調(diào)用外部函數(shù)都會(huì)為局部變量創(chuàng)建一個(gè)單獨(dú)的閉包。不是每個(gè)函數(shù)聲明都有單獨(dú)閉包,而是每次函數(shù)調(diào)用都會(huì)創(chuàng)建一個(gè)閉包。

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

總結(jié)

如果對(duì)閉包并不完全明白,那么最好的辦法就是回過頭研究研究這些案例。我對(duì)閉包和堆棧幀等概念的解釋可能在專業(yè)上并不完全正規(guī),這都是為了幫助大家更好地理解。一旦這些基礎(chǔ)知識(shí)得到掌握,你可以在以后的日子里去摳那些更專業(yè)的細(xì)節(jié)。

最后幾點(diǎn)

  • 任何時(shí)候,你在一個(gè)函數(shù)體內(nèi)用了另外一個(gè)函數(shù),閉包就產(chǎn)生了。
  • 任何時(shí)候,你在一個(gè)函數(shù)體內(nèi)用了eval(),閉包就產(chǎn)生了。在eval中的內(nèi)容可以引用函數(shù)里的局部變量,你甚至可以在eval內(nèi)聲明新的局部變量,比如:eval('var foo = ...')。
  • 當(dāng)你在一個(gè)函數(shù)體內(nèi)使用構(gòu)造函數(shù)(new Function(...)),不會(huì)產(chǎn)生閉包(這個(gè)構(gòu)造函數(shù)不能引用外部函數(shù)的局部變量)。
  • Javascript中的閉包就像外部函數(shù)返回后,用來保存所有局部變量的存儲(chǔ)副本一樣。
  • 最好可以這樣認(rèn)為:閉包只是一個(gè)函數(shù)的入口,函數(shù)的局部變量被添加到這個(gè)閉包中。
  • 每次調(diào)用一個(gè)帶有閉包的函數(shù)時(shí),都會(huì)保存一組新的局部變量(假定該函數(shù)內(nèi)包含一個(gè)函數(shù)聲明,并且返回到外部,或者以某種方式為其保留外部引用)。
  • 兩個(gè)函數(shù)可能看起來代碼相同,但是由于“隱藏”的閉包,它們有著完全不同的行為。我并不認(rèn)為通過Javascript代碼可以很容易看出一個(gè)函數(shù)引用是否擁有閉包。
  • 如果你想進(jìn)行動(dòng)態(tài)修改代碼(比如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));),如果myFunction是閉包,將行不通(當(dāng)然,你應(yīng)該永遠(yuǎn)不會(huì)想要這樣進(jìn)行源代碼字符串替換,但是……)。
  • 很可能出現(xiàn)這種情況:在函數(shù)體內(nèi)的函數(shù)聲明中還有函數(shù)聲明,那么你會(huì)發(fā)現(xiàn)有不止一個(gè)層級(jí)上的閉包出現(xiàn)。
  • 我懷疑Javascript中的閉包和那些函數(shù)式語言的閉包不同。

閉包的應(yīng)用(譯者注)

  • 實(shí)現(xiàn)封裝,私有化屬性/變量
  • 模塊化開發(fā),防止全局污染
  • 用作緩存
  • 用作公有變量
  • 等等……

閉包的危害(譯者注)

閉包會(huì)導(dǎo)致原有作用域鏈不釋放,造成內(nèi)存泄露。

感謝

如果你剛剛學(xué)會(huì)了閉包(在這篇文章或者其他地方),歡迎提出任何意見或建議,因?yàn)槟愕姆答伩赡軙?huì)使這篇文章更加清晰完善,為更多有需要的人帶來方便。我不是Javascript專家也不是閉包專家,歡迎批評(píng)指正。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容