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