Javascript閉包并非魔法

1.jpg

本文翻譯自JavaScript closures for beginners

閉包不是什么魔法

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

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

一旦深刻理解了核心概念尝苇,閉包就并不難分析和運用了雳旅。

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

兩句話總結(jié):

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

以下代碼返回一個函數(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程序員都了解如何將一個函數(shù)的引用賦給上述代碼中的變量say2殖卑。如果你不了解,那么在學(xué)習(xí)閉包之前你需要先了解一下坊萝。一個使用C的程序員會將其看作是返回指向某函數(shù)的指針孵稽,會認(rèn)為變量saysay2都是指向函數(shù)的指針。
C語言指向函數(shù)的指針和JavaScript的函數(shù)引用之間存在著很關(guān)鍵的區(qū)別十偶。在JavaScript中菩鲜,您可以將函數(shù)引用變量看作既包含指向函數(shù)的指針,也包含指向閉包的隱藏指針惦积。

上述代碼中存在一個閉包接校,因為匿名函數(shù)function() { console.log(text); }在另一個函數(shù)sayHello2()中聲明。在這個例子中狮崩,如果你在另一個函數(shù)體里使用function關(guān)鍵字蛛勉,那么你正在創(chuàng)建閉包。

在C語言和其他大多數(shù)類似語言中厉亏,在函數(shù)返回后董习,所有局部變量不可再被訪問烈和,因為堆棧幀已經(jīng)被銷毀了爱只。

而在Javascript語言中,如果你在一個函數(shù)體內(nèi)再聲明一個函數(shù)招刹,這個函數(shù)被返回到了全局恬试,局部變量依然可以被訪問窝趣。如上面所示,我們在函數(shù)sayHello2()返回后調(diào)用了函數(shù)say2()训柴,請注意哑舒,變量text是函數(shù)sayHello2()的局部變量。

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

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

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

更多案例

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

Example 3

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

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

所有這三個全局函數(shù)都有一個對同一個閉包的共同引用刚操,因為它們都是在同一個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

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

Example 5

這個案例對于許多人來說是一個大難題,你需要仔細(xì)理解一下勾徽。如果你要在一個循環(huán)體中定義一個函數(shù)滑凉,要非常小心,閉包中的局部變量可不會想你想當(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])} );所示畅姊,將一個匿名函數(shù)的引用添加到result數(shù)組中三次咒钟。如果你對匿名函數(shù)不熟悉,也可當(dāng)成如下:

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

請注意若未,當(dāng)案例執(zhí)行時朱嘴,"item2 undefined"會輸出三次!這是因為跟之前案例一樣粗合,buildList的局部變量只有一個閉包萍嬉。當(dāng)在執(zhí)行fnList[j]()調(diào)用匿名函數(shù)時,三個匿名函數(shù)都共用一個閉包隙疚,并且它們使用的是循環(huán)結(jié)束后的當(dāng)前值作為該閉包中的iitem(循環(huán)已完成帚湘,i的值為3,item值為"item2")甚淡。請注意大诸,該循環(huán)從0開始索引,到循環(huán)結(jié)束前item值為"item2"贯卦,而i ++會將i值增加到3资柔。

Example 6

此案例顯示:在外部函數(shù)退出前,外部函數(shù)內(nèi)聲明的所有全局變量都包含在閉包內(nèi)撵割。請注意贿堰,變量alice實際上是在匿名函數(shù)之后聲明的,匿名函數(shù)是最先聲明的啡彬,當(dāng)該函數(shù)被調(diào)用時羹与,它仍然可以訪問alice變量,因為該變量處于相同作用域內(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

最后這個案例表明峻呕,每次調(diào)用外部函數(shù)都會為局部變量創(chuàng)建一個單獨的閉包利职。不是每個函數(shù)聲明都有單獨閉包,而是每次函數(shù)調(diào)用都會創(chuàng)建一個閉包瘦癌。

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é)

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

最后幾點

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

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

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

閉包的危害(譯者注)

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

感謝

如果你剛剛學(xué)會了閉包(在這篇文章或者其他地方)芽隆,歡迎提出任何意見或建議浊服,因為你的反饋可能會使這篇文章更加清晰完善统屈,為更多有需要的人帶來方便。我不是Javascript專家也不是閉包專家牙躺,歡迎批評指正愁憔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市孽拷,隨后出現(xiàn)的幾起案子吨掌,更是在濱河造成了極大的恐慌,老刑警劉巖脓恕,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膜宋,死亡現(xiàn)場離奇詭異,居然都是意外死亡炼幔,警方通過查閱死者的電腦和手機秋茫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乃秀,“玉大人肛著,你說我怎么就攤上這事《逖叮” “怎么了策泣?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抬吟。 經(jīng)常有香客問我萨咕,道長,這世上最難降的妖魔是什么火本? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任危队,我火速辦了婚禮,結(jié)果婚禮上钙畔,老公的妹妹穿的比我還像新娘茫陆。我一直安慰自己,他們只是感情好擎析,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布簿盅。 她就那樣靜靜地躺著,像睡著了一般揍魂。 火紅的嫁衣襯著肌膚如雪桨醋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天现斋,我揣著相機與錄音喜最,去河邊找鬼。 笑死庄蹋,一個胖子當(dāng)著我的面吹牛瞬内,可吹牛的內(nèi)容都是我干的迷雪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼虫蝶,長吁一口氣:“原來是場噩夢啊……” “哼章咧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起能真,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赁严,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后舟陆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體误澳,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡耻矮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年秦躯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裆装。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡踱承,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哨免,到底是詐尸還是另有隱情茎活,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布琢唾,位于F島的核電站载荔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏采桃。R本人自食惡果不足惜懒熙,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望普办。 院中可真熱鬧工扎,春花似錦、人聲如沸衔蹲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舆驶。三九已至橱健,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沙廉,已是汗流浹背畴博。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蓝仲,地道東北人俱病。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓官疲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亮隙。 傳聞我的和親對象是個殘疾皇子途凫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內(nèi)容