前言
總括 :這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請自行忽略舌仍。
譯者 :文章寫在2006年像屋,可直到翻譯的21小時之前作者還在完善這篇文章端考,在Stackoverflow的How do JavaScript closures work?這個問題里更是得到了4000+的贊同紫新,文章內容質量自然不必多說。
原文地址:JavaScript Closures for Beginners
原文作者:Morris
譯者:Damonare
譯者博客地址:Damonare的個人博客
譯文地址: Javascript閉包入門(譯文)
本文屬于譯文
正文
閉包并不是魔法
這篇文章使用有效的javascript代碼向程序員們解釋了閉包担平,大牛和功能型程序員請自行忽略示绊。
實際上一旦你對閉包的核心概念心領神會了锭部,閉包就不難理解了,但如果你想通過讀那些學術性文章或是學院派的論文來理解閉包那基本是不可能的面褐。
本文主要是面向那些有主流程序語言開發(fā)經(jīng)驗或是能看懂下面這段代碼的程序員:
function sayHello(name) {
?var text = 'Hello ' + name;
?var say = function() { console.log(text); }
?say();
}
sayHello('Joe');
一個閉包小案例
兩種方式概括:
閉包是javascript支持頭等函數(shù)的一種方式拌禾,它是一個能夠引用其內部作用域變量(在本作用域第一次聲明的變量)的表達式,這個表達式可以賦值給某個變量盆耽,可以作為參數(shù)傳遞給函數(shù)蹋砚,也可以作為一個函數(shù)返回值返回。
或是
閉包是函數(shù)開始執(zhí)行的時候被分配的一個棧幀摄杂,在函數(shù)執(zhí)行結束返回后仍不會被釋放(就好像一個棧幀被分配在堆里而不是棧里坝咐!)
下面這段代碼返回了一個指向這個函數(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的,如果你不理解析恢,那么你需要理解之后再來學習閉包墨坚。C語言程序員會認為這個函數(shù)返回一個指向某函數(shù)的指針,變量say和say2都是指向某個函數(shù)的指針映挂。
Javascript的函數(shù)引用和C語言指針相比還有一個關鍵性的不同之處泽篮,在Javascript中,一個引用函數(shù)的變量可以看做是有兩個指針柑船,一個是指向函數(shù)的指針帽撑,一個是指向閉包的隱藏指針。
上面代碼中就有一個閉包鞍时,為什么呢亏拉?因為匿名函數(shù)function() { console.log(text); }是在另一個函數(shù)(在本例中就是sayHello2()函數(shù))聲明的。在Javascript中逆巍,如果你在另一個函數(shù)中使用了function關鍵字及塘,那么你就創(chuàng)建了一個閉包。
在C語言和大多數(shù)常用程序語言中锐极,當一個函數(shù)返回后笙僚,函數(shù)內聲明的局部變量就不能再被訪問了,因為該函數(shù)對應的棧幀已經(jīng)被銷毀了灵再。
在Javscript中肋层,如果你在一個函數(shù)中聲明了另一個函數(shù),那么在你調用這個函數(shù)返回后里面的局部變量仍然是可以訪問的翎迁。這個已經(jīng)在上面的代碼中演示過了栋猖,即我們在函數(shù)sayHello()返回后仍然可以調用函數(shù)say2()。注意:我們在代碼中引用的變量text是我們在函數(shù)sayHello2()中聲明的局部變量鸳兽。
function() { console.log(text); } // 輸出say2.toString();
觀察say2.toString()的輸出,我們可以看到確實引用了text變量罕拂。匿名函數(shù)之所以可以引用包含'Hello Bob'的text變量就是因為sayhello2()的局部變量被保存在了閉包中揍异。
神奇的是全陨,在JavaScript中,函數(shù)引用還有一個對于它所創(chuàng)建的閉包的秘密引用衷掷,類似于事件委托是一個方法指針加上對于某個對象的秘密引用辱姨。
更多例子
出于某種不得而知的原因,當你去閱讀一些關于閉包的文章的時候戚嗅,閉包看起來真的是難以理解的雨涛。但如果你看到一些你能夠去操作的閉包小案例(這花費了我一段時間),閉包就容易理解了懦胞。推薦好好推敲下這幾個小案例直到你徹底理解了它們到底是如何工作的替久。如果你沒完全弄明白閉包是如何工作的就去盲目使用閉包,會搞出很多神奇的bug的躏尉!
例3
局部變量雖然沒有被復制蚯根,但可以通過被引用而被保留下來。這就好像外部函數(shù)退出后胀糜,但棧幀依舊保存在內存中一樣颅拦。
function say667() {
?// 局部變量num最后會保存在閉包中
?var num = 42;
?var say = function() { console.log(num); }
?num++;
?return say;
}
var sayNumber = say667();
sayNumber(); // 輸出 43
例4
下面三個全局函數(shù)對同一個閉包有一個共同的引用,因為他們都是在調用函數(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ù)具有對同一個閉包的共享訪問權限——這個閉包是指當三個函數(shù)定義時setupSomeGlobals()的局部變量距帅。
注意:在上述示例中,當你再次調用setupSomeGlobals()時括堤,一個新的閉包(棧幀)就被創(chuàng)建了碌秸。舊變量gLogNumber, gIncreaseNumber, gSetNumber 被有新閉包的函數(shù)覆蓋(在JavaScript中,如果你在一個函數(shù)中聲明了一個新的函數(shù)痊臭,那么當外部函數(shù)被調用時哮肚,內部函數(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的局部變量對應的閉包只有一個。當匿名函數(shù)在fnlist[j]()這一行調用的時候绽乔,他們使用同一個閉包弧蝇,而且是使用的這個閉包里i和item現(xiàn)在的值(循環(huán)結束后i的值為3,item的值為'item2')。注意:我們從索引0開始看疗,所以item最后的值為item2'沙峻,i的值會被i++增加到3 。
例6
這個例子表明了閉包會保存函數(shù)退出之前內部定義的所有的局部變量两芳。注意:變量alice是在匿名函數(shù)之前創(chuàng)建的摔寨。 匿名函數(shù)先被聲明,然后當它被調用的時候之所以能夠訪問alice是因為他們在同一個作用域內(JavaScript做了變量提升)怖辆,sayAlice()()直接調用了從sayAlice()中返回的函數(shù)引用——這個和前面的完全一樣是复,只是少了臨時的變量【譯者注:存儲sayAlice()返回的函數(shù)引用的變量】
function sayAlice() {
? ?var say = function() { console.log(alice); }
? ?// 局部變量最后保存在閉包中
? ?var alice = 'Hello Alice';
? ?return say;
}
sayAlice()();// 輸出"Hello Alice"
技巧:需要注意變量say也是在閉包內部,也能被在sayAlice()內部聲明的其它函數(shù)訪問竖螃,或者也可以在函數(shù)內部遞歸訪問它淑廊。
例7
最后一個例子說明了每次調用函數(shù)都會為局部變量創(chuàng)建一個閉包。實際上每次函數(shù)聲明并不會創(chuàng)建一個單獨的閉包斑鼻,但每次調用函數(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;
總結
如果任何不太明白的地方最好的方式就是把玩這幾個例子,去機械地閱讀一些文章遠比去做這些實例難得多坚弱。我關于閉包的說明蜀备、棧框體(stack-frame)的說明等等荒叶,嚴格理論上講并不是完全正確的——它們只是為了理解而簡化處理過的碾阁。當基礎的概念心領神會之后,就可以輕松地理解這些細節(jié)了些楣。
最終總結
每當你在另一個函數(shù)里使用了關鍵字function脂凶,一個閉包就被創(chuàng)建了
每當你在一個函數(shù)內部使用了eval(),一個閉包就被創(chuàng)建了愁茁。在eval內部你可以引用外部函數(shù)定義的局部變量蚕钦,同樣的,在eval內部也可以通過eval('var foo = …')來創(chuàng)建新的局部變量鹅很。
當你在一個函數(shù)內部使用new function(...)(即構造函數(shù))時嘶居,它不會創(chuàng)建閉包(新函數(shù)不能引用外部函數(shù)的局部變量)。
JavaScript中的閉包促煮,就像一個副本邮屁,將某函數(shù)在退出時候的所有局部變量復制保存其中。
也許最好的理解是閉包總是在進入某個函數(shù)的時候被創(chuàng)建菠齿,而局部變量是被加入到這個閉包中佑吝。
閉包函數(shù)每次被調用的時候都會創(chuàng)建一組新的局部變量存儲。(前提是這個函數(shù)包含一個內部的函數(shù)聲明绳匀,并且這個函數(shù)的引用被返回或者用某種方法被存儲到一個外部的引用中)
兩個函數(shù)或許從源代碼文本上看起來一樣芋忿,但因為隱藏閉包的存在會讓兩個函數(shù)具有不同的行為炸客。我認為Javascript代碼實際上并不能找出一個函數(shù)引用是否有閉包。
如果你正嘗試做一些動態(tài)源代碼的修改(例如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));)戈钢,如果myFunction是一個閉包的話嚷量,那么這并不會生效(當然,你甚至可能從來都沒有在運行的時候考慮過修改源代碼字符串逆趣,但是。嗜历。宣渗。)。
在函數(shù)內部的函數(shù)的內部聲明函數(shù)是可以的——可以獲得不止一個層級的閉包梨州。
通常我認為閉包是一個同時包含函數(shù)和被捕捉的變量的術語痕囱,但是請注意我并沒有在本文中使用這個定義。
我覺得JavaScript中的閉包跟其它函數(shù)式編程語言中的閉包是有不同之處的暴匠。
感謝
如果你正好在學習閉包(在這里或是其他地方)鞍恢,期待您對本文的任何反饋,您的任何建議都可能會使本文更加清晰易懂每窖。請聯(lián)系jztan1996@gmail.com 【譯者注:這是譯者的郵箱帮掉,歡迎交流學習】
后記
這是譯者翻譯的第一篇文章,收獲良多窒典,感覺上并不比自己寫一篇文章省事蟆炊,相反熟悉內容了解代碼的同時還得去揣摩作者表達的意圖,難度的確要比自己單獨寫一篇高瀑志。能力有限涩搓,水平一般,有翻譯不到位的地方劈猪,歡迎批評指正昧甘。感謝!
?
轉載于 2017-01-21