Javascript閉包入門(譯文)

前言

總括 :這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請自行忽略装哆。

譯者 :文章寫在2006年肋演,可直到翻譯的21小時之前作者還在完善這篇文章,在Stackoverflow的How do JavaScript closures work?這個問題里更是得到了4000+的贊同烂琴,文章內(nèi)容質(zhì)量自然不必多說。

本文屬于譯文

正文

閉包并不是魔法

這篇文章使用有效的javascript代碼向程序員們解釋了閉包蜕乡,大牛和功能型程序員請自行忽略奸绷。

實際上一旦你對閉包的核心概念心領(lǐng)神會了,閉包就不難理解了层玲,但如果你想通過讀那些學(xué)術(shù)性文章或是學(xué)院派的論文來理解閉包那基本是不可能的号醉。

本文主要是面向那些有主流程序語言開發(fā)經(jīng)驗或是能看懂下面這段代碼的程序員:

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

一個閉包小案例

兩種方式概括:

  • 閉包是javascript支持頭等函數(shù)的一種方式,它是一個能夠引用其內(nèi)部作用域變量(在本作用域第一次聲明的變量)的表達式辛块,這個表達式可以賦值給某個變量畔派,可以作為參數(shù)傳遞給函數(shù),也可以作為一個函數(shù)返回值返回润绵。

或是

  • 閉包是函數(shù)開始執(zhí)行的時候被分配的一個棧幀线椰,在函數(shù)執(zhí)行結(jié)束返回后仍不會被釋放(就好像一個棧幀被分配在堆里而不是棧里!)

下面這段代碼返回了一個指向這個函數(shù)的引用:

function sayHello2(name) {
  var text = 'Hello ' + name; // 局部變量text
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // 打印日志: "Hello Bob"

絕大部分Javascript程序員能夠理解上面代碼中的一個函數(shù)引用是如何返回賦值給變量say2的尘盼,如果你不理解憨愉,那么你需要理解之后再來學(xué)習閉包烦绳。C語言程序員會認為這個函數(shù)返回一個指向某函數(shù)的指針,變量saysay2都是指向某個函數(shù)的指針配紫。

Javascript的函數(shù)引用和C語言指針相比還有一個關(guān)鍵性的不同之處径密,在Javascript中,一個引用函數(shù)的變量可以看做是有兩個指針躺孝,一個是指向函數(shù)的指針享扔,一個是指向閉包的隱藏指針。

上面代碼中就有一個閉包植袍,為什么呢惧眠?因為匿名函數(shù)function() { console.log(text); }是在另一個函數(shù)(在本例中就是sayHello2()函數(shù))聲明的。在Javascript中奋单,如果你在另一個函數(shù)中使用了function關(guān)鍵字锉试,那么你就創(chuàng)建了一個閉包。

在C語言和大多數(shù)常用程序語言中览濒,當一個函數(shù)返回后呆盖,函數(shù)內(nèi)聲明的局部變量就不能再被訪問了,因為該函數(shù)對應(yīng)的棧幀已經(jīng)被銷毀了贷笛。

在Javscript中应又,如果你在一個函數(shù)中聲明了另一個函數(shù),那么在你調(diào)用這個函數(shù)返回后里面的局部變量仍然是可以訪問的乏苦。這個已經(jīng)在上面的代碼中演示過了株扛,即我們在函數(shù)sayHello()返回后仍然可以調(diào)用函數(shù)say2()注意:我們在代碼中引用的變量text是我們在函數(shù)sayHello2()中聲明的局部變量汇荐。

function() { console.log(text); } // 輸出say2.toString();

觀察say2.toString()的輸出洞就,我們可以看到確實引用了text變量。匿名函數(shù)之所以可以引用包含'Hello Bob'text變量就是因為sayhello2()的局部變量被保存在了閉包中掀淘。

神奇的是旬蟋,在JavaScript中,函數(shù)引用還有一個對于它所創(chuàng)建的閉包的秘密引用革娄,類似于事件委托是一個方法指針加上對于某個對象的秘密引用倾贰。

更多例子

出于某種不得而知的原因,當你去閱讀一些關(guān)于閉包的文章的時候拦惋,閉包看起來真的是難以理解的匆浙。但如果你看到一些你能夠去操作的閉包小案例(這花費了我一段時間),閉包就容易理解了厕妖。推薦好好推敲下這幾個小案例直到你徹底理解了它們到底是如何工作的首尼。如果你沒完全弄明白閉包是如何工作的就去盲目使用閉包,會搞出很多神奇的bug的!

例3

局部變量雖然沒有被復(fù)制饰恕,但可以通過被引用而被保留下來挠羔。這就好像外部函數(shù)退出后,但棧幀依舊保存在內(nèi)存中一樣埋嵌。

function say667() {
  // 局部變量num最后會保存在閉包中
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // 輸出 43

例4

下面三個全局函數(shù)對同一個閉包有一個共同的引用破加,因為他們都是在調(diào)用函數(shù)setupSomeGlobals()時聲明的。

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // 局部變量num最后會保存在閉包中
  var num = 42;
  // 將一些對于函數(shù)的引用存儲為全局變量
  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ù)具有對同一個閉包的共享訪問權(quán)限——這個閉包是指當三個函數(shù)定義時setupSomeGlobals()的局部變量雹嗦。

注意:在上述示例中范舀,當你再次調(diào)用setupSomeGlobals()時,一個新的閉包(棧幀)就被創(chuàng)建了了罪。舊變量gLogNumber, gIncreaseNumber, gSetNumber 被有新閉包的函數(shù)覆蓋(在JavaScript中锭环,如果你在一個函數(shù)中聲明了一個新的函數(shù),那么當外部函數(shù)被調(diào)用時泊藕,內(nèi)部函數(shù)會被重新創(chuàng)建)辅辩。

例5

這個示例對于很多人來說都是一個挑戰(zhàn),所以希望你能弄懂它娃圆。注意:當你在一個循環(huán)里面定義一個函數(shù)的時候玫锋,閉包里的局部變量可能不會像你想的那樣。

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]);
    // 使用j是為了防止搞混---可以使用i
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}
 testList() //輸出 "item2 undefined" 3 次

result.push( function() {console.log(item + ' ' + list[i])}這一行給result數(shù)組添加了三次函數(shù)匿名引用讼呢。如果你不熟悉匿名函數(shù)可以想象成下面代碼:

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

注意撩鹿,當你運行上述代碼的時候會打印"item2 undefined"三次!和前面的示例一樣悦屏,和buildList的局部變量對應(yīng)的閉包只有一個节沦。當匿名函數(shù)在fnlist[j]()這一行調(diào)用的時候,他們使用同一個閉包础爬,而且是使用的這個閉包里iitem現(xiàn)在的值(循環(huán)結(jié)束后i的值為3甫贯,item的值為'item2')。注意:我們從索引0開始看蚜,所以item最后的值為item2'叫搁,i的值會被i++增加到3

例6

這個例子表明了閉包會保存函數(shù)退出之前內(nèi)部定義的所有的局部變量失乾。注意:變量alice是在匿名函數(shù)之前創(chuàng)建的。 匿名函數(shù)先被聲明纬乍,然后當它被調(diào)用的時候之所以能夠訪問alice是因為他們在同一個作用域內(nèi)(JavaScript做了變量提升)碱茁,sayAlice()()直接調(diào)用了從sayAlice()中返回的函數(shù)引用——這個和前面的完全一樣,只是少了臨時的變量【譯者注:存儲sayAlice()返回的函數(shù)引用的變量】

function sayAlice() {
    var say = function() { console.log(alice); }
    // 局部變量最后保存在閉包中
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// 輸出"Hello Alice"

技巧:需要注意變量say也是在閉包內(nèi)部仿贬,也能被在sayAlice()內(nèi)部聲明的其它函數(shù)訪問纽竣,或者也可以在函數(shù)內(nèi)部遞歸訪問它。

例7

最后一個例子說明了每次調(diào)用函數(shù)都會為局部變量創(chuàng)建一個閉包。實際上每次函數(shù)聲明并不會創(chuàng)建一個單獨的閉包蜓氨,但每次調(diào)用函數(shù)都會創(chuàng)建一個獨立的閉包聋袋。

function newClosure(someNum, someRef) {
    // 局部變量最終保存在閉包中
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '\nanArray ' + anArray.toString() +
            '\nref.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é)

如果任何不太明白的地方最好的方式就是把玩這幾個例子,去機械地閱讀一些文章遠比去做這些實例難得多穴吹。我關(guān)于閉包的說明幽勒、棧框體(stack-frame)的說明等等港令,嚴格理論上講并不是完全正確的——它們只是為了理解而簡化處理過的啥容。當基礎(chǔ)的概念心領(lǐng)神會之后,就可以輕松地理解這些細節(jié)了顷霹。

最終總結(jié)

  • 每當你在另一個函數(shù)里使用了關(guān)鍵字function咪惠,一個閉包就被創(chuàng)建了
  • 每當你在一個函數(shù)內(nèi)部使用了eval(),一個閉包就被創(chuàng)建了淋淀。在eval內(nèi)部你可以引用外部函數(shù)定義的局部變量遥昧,同樣的,在eval內(nèi)部也可以通過eval('var foo = …')來創(chuàng)建新的局部變量朵纷。
  • 當你在一個函數(shù)內(nèi)部使用new function(...)(即構(gòu)造函數(shù))時炭臭,它不會創(chuàng)建閉包(新函數(shù)不能引用外部函數(shù)的局部變量)。
  • JavaScript中的閉包柴罐,就像一個副本徽缚,將某函數(shù)在退出時候的所有局部變量復(fù)制保存其中。
  • 也許最好的理解是閉包總是在進入某個函數(shù)的時候被創(chuàng)建革屠,而局部變量是被加入到這個閉包中凿试。
  • 閉包函數(shù)每次被調(diào)用的時候都會創(chuàng)建一組新的局部變量存儲。(前提是這個函數(shù)包含一個內(nèi)部的函數(shù)聲明似芝,并且這個函數(shù)的引用被返回或者用某種方法被存儲到一個外部的引用中)
  • 兩個函數(shù)或許從源代碼文本上看起來一樣那婉,但因為隱藏閉包的存在會讓兩個函數(shù)具有不同的行為。我認為Javascript代碼實際上并不能找出一個函數(shù)引用是否有閉包党瓮。
  • 如果你正嘗試做一些動態(tài)源代碼的修改(例如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));)详炬,如果myFunction是一個閉包的話,那么這并不會生效(當然寞奸,你甚至可能從來都沒有在運行的時候考慮過修改源代碼字符串呛谜,但是。枪萄。隐岛。)。
  • 在函數(shù)內(nèi)部的函數(shù)的內(nèi)部聲明函數(shù)是可以的——可以獲得不止一個層級的閉包瓷翻。
  • 通常我認為閉包是一個同時包含函數(shù)和被捕捉的變量的術(shù)語聚凹,但是請注意我并沒有在本文中使用這個定義割坠。
  • 我覺得JavaScript中的閉包跟其它函數(shù)式編程語言中的閉包是有不同之處的。

感謝

如果你正好在學(xué)習閉包(在這里或是其他地方)妒牙,期待您對本文的任何反饋彼哼,您的任何建議都可能會使本文更加清晰易懂。請聯(lián)系<a href="mailto:jztan1996@gmail.com">jztan1996@gmail.com 【譯者注:這是譯者的郵箱湘今,歡迎交流學(xué)習】</a>

后記

這是譯者翻譯的第一篇文章敢朱,收獲良多,感覺上并不比自己寫一篇文章省事象浑,相反熟悉內(nèi)容了解代碼的同時還得去揣摩作者表達的意圖蔫饰,難度的確要比自己單獨寫一篇高。能力有限愉豺,水平一般篓吁,有翻譯不到位的地方,歡迎批評指正蚪拦。感謝杖剪!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驰贷,隨后出現(xiàn)的幾起案子盛嘿,更是在濱河造成了極大的恐慌,老刑警劉巖括袒,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件次兆,死亡現(xiàn)場離奇詭異,居然都是意外死亡锹锰,警方通過查閱死者的電腦和手機芥炭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恃慧,“玉大人园蝠,你說我怎么就攤上這事×∈浚” “怎么了彪薛?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長怠蹂。 經(jīng)常有香客問我善延,道長,這世上最難降的妖魔是什么城侧? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任易遣,我火速辦了婚禮,結(jié)果婚禮上赞庶,老公的妹妹穿的比我還像新娘训挡。我一直安慰自己,他們只是感情好歧强,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布澜薄。 她就那樣靜靜地躺著,像睡著了一般摊册。 火紅的嫁衣襯著肌膚如雪肤京。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天茅特,我揣著相機與錄音忘分,去河邊找鬼。 笑死白修,一個胖子當著我的面吹牛妒峦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播兵睛,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼肯骇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祖很?” 一聲冷哼從身側(cè)響起笛丙,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎假颇,沒想到半個月后胚鸯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡笨鸡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年姜钳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镜豹。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡傲须,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出趟脂,到底是詐尸還是另有隱情泰讽,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布昔期,位于F島的核電站已卸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏硼一。R本人自食惡果不足惜累澡,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望般贼。 院中可真熱鬧愧哟,春花似錦奥吩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肥矢,卻和暖如春端衰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甘改。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工旅东, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人十艾。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓抵代,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忘嫉。 傳聞我的和親對象是個殘疾皇子主守,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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