摘要:也許最好的理解是閉包總是在進(jìn)入某個(gè)函數(shù)的時(shí)候被創(chuàng)建,而局部變量是被加入到這個(gè)閉包中。在函數(shù)內(nèi)部的函數(shù)的內(nèi)部聲明函數(shù)是可以的可以獲得不止一個(gè)層級(jí)的閉包。
前言
總括 :這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請(qǐng)自行忽略。
譯者 :文章寫在2006年,可直到翻譯的21小時(shí)之前作者還在完善這篇文章,在Stackoverflow的How do JavaScript closures work?這個(gè)問題里更是得到了4000+的贊同,文章內(nèi)容自然不必多說。
原文地址:JavaScript Closures for Beginners
原文作者:Morris?
譯者:Damonare
譯者博客地址:Damonare的個(gè)人博客
譯文地址: Javascript閉包入門(譯文)
本文屬于譯文
正文 閉包并不是魔法這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請(qǐng)自行忽略。
實(shí)際上一旦你對(duì)閉包的核心概念心領(lǐng)神會(huì)了,閉包就不難理解了,但如果你想通過讀那些學(xué)術(shù)性文章或是學(xué)院派的論文來理解閉包那基本是不可能的。
本文主要是面向那些有主流程序語言開發(fā)經(jīng)驗(yàn)或是能看懂下面這段代碼的程序員:
function sayHello(name) { var text = "Hello " + name; var say = function() { console.log(text); } say(); } sayHello("Joe");一個(gè)閉包小案例
兩種方式概括:
閉包是javascript支持頭等函數(shù)的一種方式,它是一個(gè)能夠引用其內(nèi)部作用域變量(在本作用域第一次聲明的變量)的表達(dá)式,這個(gè)表達(dá)式可以賦值給某個(gè)變量,可以作為參數(shù)傳遞給函數(shù),也可以作為一個(gè)函數(shù)返回值返回。
或是
閉包是函數(shù)開始執(zhí)行的時(shí)候被分配的一個(gè)棧幀,在函數(shù)執(zhí)行結(jié)束返回后仍不會(huì)被釋放(就好像一個(gè)棧幀被分配在堆里而不是棧里!)
下面這段代碼返回了一個(gè)指向這個(gè)函數(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程序員能夠理解上面代碼中的一個(gè)函數(shù)引用是如何返回賦值給變量say2的,如果你不理解,那么你需要理解之后再來學(xué)習(xí)閉包。C語言程序員會(huì)認(rèn)為這個(gè)函數(shù)返回一個(gè)指向某函數(shù)的指針,變量say和say2都是指向某個(gè)函數(shù)的指針。
Javascript的函數(shù)引用和C語言指針相比還有一個(gè)關(guān)鍵性的不同之處,在Javascript中,一個(gè)引用函數(shù)的變量可以看做是有兩個(gè)指針,一個(gè)是指向函數(shù)的指針,一個(gè)是指向閉包的隱藏指針。
上面代碼中就有一個(gè)閉包,為什么呢?因?yàn)槟涿瘮?shù) function() { console.log(text); }是在另一個(gè)函數(shù)(在本例中就是sayHello2()函數(shù))聲明的。在Javascript中,如果你在另一個(gè)函數(shù)中使用了function關(guān)鍵字,那么你就創(chuàng)建了一個(gè)閉包。
在C語言和大多數(shù)常用程序語言中,當(dāng)一個(gè)函數(shù)返回后,函數(shù)內(nèi)聲明的局部變量就不能再被訪問了,因?yàn)樵摵瘮?shù)對(duì)應(yīng)的棧幀已經(jīng)被銷毀了。
在Javscript中,如果你在一個(gè)函數(shù)中聲明了另一個(gè)函數(shù),那么在你調(diào)用這個(gè)函數(shù)返回后里面的局部變量仍然是可以訪問的。這個(gè)已經(jīng)在上面的代碼中演示過了,即我們?cè)诤瘮?shù)sayHello()返回后仍然可以調(diào)用函數(shù)say2()。注意:我們?cè)诖a中引用的變量text是我們?cè)诤瘮?shù)sayHello2()中聲明的局部變量。
function() { console.log(text); } // 輸出say2.toString();
觀察say2.toString()的輸出,我們可以看到確實(shí)引用了text變量。匿名函數(shù)之所以可以引用包含"Hello Bob"的text變量就是因?yàn)?b>sayhello2()的局部變量被保存在了閉包中。
神奇的是,在JavaScript中,函數(shù)引用還有一個(gè)對(duì)于它所創(chuàng)建的閉包的秘密引用,類似于事件委托是一個(gè)方法指針加上對(duì)于某個(gè)對(duì)象的秘密引用。
更多例子出于某種不得而知的原因,當(dāng)你去閱讀一些關(guān)于閉包的文章的時(shí)候,閉包看起來真的是難以理解的。但如果你看到一些你能夠去操作的閉包小案例(這花費(fèi)了我一段時(shí)間),閉包就容易理解了。推薦好好推敲下這幾個(gè)小案例直到你徹底理解了它們到底是如何工作的。如果你沒完全弄明白閉包是如何工作的就去盲目使用閉包,會(huì)搞出很多神奇的bug的!
例3局部變量雖然沒有被復(fù)制,但可以通過被引用而被保留下來。這就好像外部函數(shù)退出后,但棧幀依舊保存在內(nèi)存中一樣。
function say667() { // 局部變量num最后會(huì)保存在閉包中 var num = 42; var say = function() { console.log(num); } num++; return say; } var sayNumber = say667(); sayNumber(); // 輸出 43例4
下面三個(gè)全局函數(shù)對(duì)同一個(gè)閉包有一個(gè)共同的引用,因?yàn)樗麄兌际窃谡{(diào)用函數(shù)setupSomeGlobals()時(shí)聲明的。
var gLogNumber, gIncreaseNumber, gSetNumber; function setupSomeGlobals() { // 局部變量num最后會(huì)保存在閉包中 var num = 42; // 將一些對(duì)于函數(shù)的引用存儲(chǔ)為全局變量 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ù)具有對(duì)同一個(gè)閉包的共享訪問權(quán)限——這個(gè)閉包是指當(dāng)三個(gè)函數(shù)定義時(shí)setupSomeGlobals()的局部變量。
注意:在上述示例中,當(dāng)你再次調(diào)用setupSomeGlobals()時(shí),一個(gè)新的閉包(棧幀)就被創(chuàng)建了。舊變量gLogNumber,?gIncreaseNumber,?gSetNumber?被有新閉包的函數(shù)覆蓋(在JavaScript中,如果你在一個(gè)函數(shù)中聲明了一個(gè)新的函數(shù),那么當(dāng)外部函數(shù)被調(diào)用時(shí),內(nèi)部函數(shù)會(huì)被重新創(chuàng)建)。
例5這個(gè)示例對(duì)于很多人來說都是一個(gè)挑戰(zhàn),所以希望你能弄懂它。注意:當(dāng)你在一個(gè)循環(huán)里面定義一個(gè)函數(shù)的時(shí)候,閉包里的局部變量可能不會(huì)像你想的那樣。
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);
注意,當(dāng)你運(yùn)行上述代碼的時(shí)候會(huì)打印"item2 undefined"三次!和前面的示例一樣,和buildList的局部變量對(duì)應(yīng)的閉包只有一個(gè)。當(dāng)匿名函數(shù)在fnlist[j]()這一行調(diào)用的時(shí)候,他們使用同一個(gè)閉包,而且是使用的這個(gè)閉包里i和item現(xiàn)在的值(循環(huán)結(jié)束后i的值為3,item的值為"item2")。注意:我們從索引0開始,所以item最后的值為item2",i的值會(huì)被i++增加到3 。
例6這個(gè)例子表明了閉包會(huì)保存函數(shù)退出之前內(nèi)部定義的所有的局部變量。注意:變量alice是在匿名函數(shù)之前創(chuàng)建的。 匿名函數(shù)先被聲明,然后當(dāng)它被調(diào)用的時(shí)候之所以能夠訪問alice是因?yàn)樗麄冊(cè)谕粋€(gè)作用域內(nèi)(JavaScript做了變量提升),sayAlice()()直接調(diào)用了從sayAlice()中返回的函數(shù)引用——這個(gè)和前面的完全一樣,只是少了臨時(shí)的變量【譯者注:存儲(chǔ)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最后一個(gè)例子說明了每次調(diào)用函數(shù)都會(huì)為局部變量創(chuàng)建一個(gè)閉包。實(shí)際上每次函數(shù)聲明并不會(huì)創(chuàng)建一個(gè)多帶帶的閉包,但每次調(diào)用函數(shù)都會(huì)創(chuàng)建一個(gè)獨(dú)立的閉包。
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 + " 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é)
如果任何不太明白的地方最好的方式就是把玩這幾個(gè)例子,去機(jī)械地閱讀一些文章遠(yuǎn)比去做這些實(shí)例難得多。我關(guān)于閉包的說明、??蝮w(stack-frame)的說明等等,嚴(yán)格理論上講并不是完全正確的——它們只是為了理解而簡化處理過的。當(dāng)基礎(chǔ)的概念心領(lǐng)神會(huì)之后,就可以輕松地理解這些細(xì)節(jié)了。
最終總結(jié)每當(dāng)你在另一個(gè)函數(shù)里使用了關(guān)鍵字function,一個(gè)閉包就被創(chuàng)建了
每當(dāng)你在一個(gè)函數(shù)內(nèi)部使用了eval(),一個(gè)閉包就被創(chuàng)建了。在eval內(nèi)部你可以引用外部函數(shù)定義的局部變量,同樣的,在eval內(nèi)部也可以通過eval("var foo = …")來創(chuàng)建新的局部變量。
當(dāng)你在一個(gè)函數(shù)內(nèi)部使用new function(...)(即構(gòu)造函數(shù))時(shí),它不會(huì)創(chuàng)建閉包(新函數(shù)不能引用外部函數(shù)的局部變量)。
JavaScript中的閉包,就像一個(gè)副本,將某函數(shù)在退出時(shí)候的所有局部變量復(fù)制保存其中。
也許最好的理解是閉包總是在進(jìn)入某個(gè)函數(shù)的時(shí)候被創(chuàng)建,而局部變量是被加入到這個(gè)閉包中。
閉包函數(shù)每次被調(diào)用的時(shí)候都會(huì)創(chuàng)建一組新的局部變量存儲(chǔ)。(前提是這個(gè)函數(shù)包含一個(gè)內(nèi)部的函數(shù)聲明,并且這個(gè)函數(shù)的引用被返回或者用某種方法被存儲(chǔ)到一個(gè)外部的引用中)
兩個(gè)函數(shù)或許從源代碼文本上看起來一樣,但因?yàn)殡[藏閉包的存在會(huì)讓兩個(gè)函數(shù)具有不同的行為。我認(rèn)為Javascript代碼實(shí)際上并不能找出一個(gè)函數(shù)引用是否有閉包。
如果你正嘗試做一些動(dòng)態(tài)源代碼的修改(例如:myFunction = Function(myFunction.toString().replace(/Hello/,"Hola"));),如果myFunction是一個(gè)閉包的話,那么這并不會(huì)生效(當(dāng)然,你甚至可能從來都沒有在運(yùn)行的時(shí)候考慮過修改源代碼字符串,但是。。。)。
在函數(shù)內(nèi)部的函數(shù)的內(nèi)部聲明函數(shù)是可以的——可以獲得不止一個(gè)層級(jí)的閉包。
通常我認(rèn)為閉包是一個(gè)同時(shí)包含函數(shù)和被捕捉的變量的術(shù)語,但是請(qǐng)注意我并沒有在本文中使用這個(gè)定義。
我覺得JavaScript中的閉包跟其它函數(shù)式編程語言中的閉包是有不同之處的。
感謝如果你正好在學(xué)習(xí)閉包(在這里或是其他地方),期待您對(duì)本文的任何反饋,您的任何建議都可能會(huì)使本文更加清晰易懂。請(qǐng)聯(lián)系jztan1996@gmail .com 【譯者注:這是譯者的郵箱,歡迎交流學(xué)習(xí)】
后記這是譯者翻譯的第一篇文章,收獲良多,感覺上并不比自己寫一篇文章省事,相反熟悉內(nèi)容了解代碼的同時(shí)還得去揣摩作者表達(dá)的意圖,難度的確要比自己多帶帶寫一篇高。能力有限,水平一般,有翻譯不到位的地方,歡迎批評(píng)指正。感謝!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/81323.html
摘要:今天同學(xué)去面試,做了兩道面試題全部做錯(cuò)了,發(fā)過來給道典型的面試題前端掘金在界中,開發(fā)人員的需求量一直居高不下。 排序算法 -- JavaScript 標(biāo)準(zhǔn)參考教程(alpha) - 前端 - 掘金來自《JavaScript 標(biāo)準(zhǔn)參考教程(alpha)》,by 阮一峰 目錄 冒泡排序 簡介 算法實(shí)現(xiàn) 選擇排序 簡介 算法實(shí)現(xiàn) ... 圖例詳解那道 setTimeout 與循環(huán)閉包的經(jīng)典面...
摘要:函數(shù)式編程前端掘金引言面向?qū)ο缶幊桃恢币詠矶际侵械闹鲗?dǎo)范式。函數(shù)式編程是一種強(qiáng)調(diào)減少對(duì)程序外部狀態(tài)產(chǎn)生改變的方式。 JavaScript 函數(shù)式編程 - 前端 - 掘金引言 面向?qū)ο缶幊桃恢币詠矶际荍avaScript中的主導(dǎo)范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數(shù)式編程越來越多得受到開發(fā)者的青睞。函數(shù)式編程是一種強(qiáng)調(diào)減少對(duì)程序外部狀態(tài)產(chǎn)生改變的方式。因此,...
摘要:本系列的第一篇文章著重提供一個(gè)關(guān)于引擎運(yùn)行時(shí)和調(diào)用棧的概述。在硬件層面,計(jì)算機(jī)內(nèi)存由大量的觸發(fā)器組成。每個(gè)觸發(fā)器包含幾個(gè)晶體管能夠存儲(chǔ)一個(gè)比特譯注位??梢酝ㄟ^唯一標(biāo)識(shí)符來訪問單個(gè)觸發(fā)器,所以可以對(duì)它們進(jìn)行讀寫操作。比特稱為個(gè)字節(jié)。 原文 How JavaScript works: memory management + how to handle 4 common memory lea...
摘要:本期推薦文章類內(nèi)存泄漏及如何避免,由于微信不能訪問外鏈,點(diǎn)擊閱讀原文就可以啦。四種常見的內(nèi)存泄漏劃重點(diǎn)這是個(gè)考點(diǎn)意外的全局變量未定義的變量會(huì)在全局對(duì)象創(chuàng)建一個(gè)新變量,如下。因?yàn)槔习姹镜氖菬o法檢測(cè)節(jié)點(diǎn)與代碼之間的循環(huán)引用,會(huì)導(dǎo)致內(nèi)存泄漏。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題...
摘要:理解的函數(shù)基礎(chǔ)要搞好深入淺出原型使用原型模型,雖然這經(jīng)常被當(dāng)作缺點(diǎn)提及,但是只要善于運(yùn)用,其實(shí)基于原型的繼承模型比傳統(tǒng)的類繼承還要強(qiáng)大。中文指南基本操作指南二繼續(xù)熟悉的幾對(duì)方法,包括,,。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。 怎樣使用 this 因?yàn)楸救藢儆趥吻岸?,因此文中只看懂?8 成左右,希望能夠給大家?guī)韼椭?...(據(jù)說是阿里的前端妹子寫的) this 的值到底...
閱讀 884·2019-08-30 14:05
閱讀 1773·2019-08-30 11:08
閱讀 3279·2019-08-29 15:41
閱讀 3645·2019-08-23 18:31
閱讀 1587·2019-08-23 18:29
閱讀 606·2019-08-23 14:51
閱讀 2159·2019-08-23 13:53
閱讀 2199·2019-08-23 13:02