摘要:簡要介紹閉包可謂是中的一大特色了,即使你對閉包沒概念,你可能已經(jīng)在不知不覺中使用到了閉包。這就是閉包的獨特之處。當頁面中存在過多的閉包,或者閉包的嵌套很多很深時,會導致內(nèi)存占用過多。因此,在這里建議慎用閉包。
1. 簡要介紹
閉包可謂是js中的一大特色了,即使你對閉包沒概念,你可能已經(jīng)在不知不覺中使用到了閉包。閉包是什么,閉包就是一個函數(shù)可以訪問到另一個函數(shù)的變量。這就是閉包,解釋起來就這么一句話,不明白?我們來看一個簡單的例子:
function getName(){ var name="wenzi"; setTimeout(function(){ console.log(name); }, 500); } getName();
這就其實已經(jīng)是閉包了,setTimeout中的function是一個匿名函數(shù),這個匿名函數(shù)里的name是geName()作用域中的變量,匿名函數(shù)里只有一個輸出語句:console.log();
還有一個很經(jīng)典的例子也可以幫助我們理解什么是閉包:
function create(){ var i=0; // 返回一個函數(shù),暫且稱之為函數(shù)A return function(){ i++; console.log(i); } } var c = create(); // c是一個函數(shù) c(); // 函數(shù)執(zhí)行 c(); // 再次執(zhí)行 c(); // 第三次執(zhí)行
在上面的例子中,create()返回的是一個函數(shù),我們暫且稱之為函數(shù)A吧。在函數(shù)A中,有兩條語句,一條是變量i自增(i++),一條是輸出語句(console.log)。第一次執(zhí)行執(zhí)行c()時會產(chǎn)生什么樣的結(jié)果?嗯,輸出自增后的變量i,也就是輸出1;那么第二次執(zhí)行c()呢,對,會輸出2;第三次執(zhí)行c()時會輸出3,依次累加。這個create()函數(shù)依然滿足了我們在剛開始時的定義,函數(shù)A使用到了另一個函數(shù)create()中的變量i。
可是為什么會產(chǎn)生這樣的輸出呢,為什么i就能一直自增呢,create函數(shù)已經(jīng)執(zhí)行完并返回結(jié)果了呀,可是為什么還能接著使用i呢,而且i還能自增。這里就涉及到了三個比較重要的概念,講解完這三個概念,我們對閉包就可以有一個比較好的理解了。
2. 三個重要概念 2.1 執(zhí)行環(huán)境與變量對象執(zhí)行環(huán)境是JavaScript中一個重要的概念,它決定了變量或函數(shù)是否有權(quán)訪問其他的數(shù)據(jù),決定了它們各自的行為。每個執(zhí)行環(huán)境都有一個與之對應的變量對象,執(zhí)行環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。雖然我們的代碼無法訪問這個對象,但是解析器在處理數(shù)據(jù)時會在后臺使用它。
我們用一個比較簡單的比喻來形容這兩個概念。執(zhí)行環(huán)境就是一個人,變量對象就是這個人的身份證號,每個人都有其對應的身份證號。他這個人決定了他身上的很多屬性和方法(動作),而且這個人的屬性和方法都在他的身份證號上,當這個人消亡的時候,身份證號也就隨之就注銷了。
全局執(zhí)行環(huán)境是最外層的一個執(zhí)行環(huán)境。在web瀏覽器中,全局執(zhí)行環(huán)境被認為是window對象,因為所有的全局變量和全局函數(shù)都是作為window對象的屬性和方法創(chuàng)建的。某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完畢后,該環(huán)境被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀(全局執(zhí)行環(huán)境直到應用程序退出——例如關(guān)閉網(wǎng)頁或者瀏覽器——時才會被銷毀),被垃圾回收機制回收。
每個函數(shù)都有自己的執(zhí)行環(huán)境。當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入到一個環(huán)境棧中。而在函數(shù)執(zhí)行后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。
2.2 作用域鏈作用域鏈是當代碼在一個環(huán)境中執(zhí)行時創(chuàng)建的,作用域鏈的用途就是要保證執(zhí)行環(huán)境中能有效有序的訪問所有變量和函數(shù)。作用域鏈的最前端始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象,下一個變量對象是來自其父親環(huán)境,再下一個變量對象是其父親的父親環(huán)境,直到全局執(zhí)行環(huán)境。
標識符解析是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然后逐級地向后回溯,直到找到標識符為止(如果找不到標識符,通常會導致錯誤發(fā)生)。其實,通俗的理解就是:在本作用域內(nèi)找不到變量或者函數(shù),則在其父親的作用域內(nèi)尋找,再找不到則到父親的父親作用域內(nèi)尋找,直到在全局的作用域內(nèi)尋找!
2.3 垃圾回收機制在js中有兩種垃圾收集的方式:標記清除和引用計數(shù)。
標記清除:垃圾收集器在運行時會給存儲在內(nèi)存中的所有變量都加上標記(具體的標記方式暫時就不清楚了),待變量已不被使用或者引用,去掉該標記或添加另一種標記。最后,垃圾收集器完成內(nèi)存清除工作,銷毀那些已無法訪問到的這些變量并回收他們所占用的空間。
引用計數(shù):一般來說,引用計數(shù)的含義是跟蹤記錄每個值被引用的次數(shù)。當聲明一個變量并將一個引用類型值賦給該變量時,則這個值的引用次數(shù)便是1,如果同一個值又被賦給另一個變量,則該值的引用次數(shù)加1,相反,如果包含對這個值引用的變量又取得了另一個值,則這個值的引用次數(shù)減1。當這個值的引用次數(shù)為0時,說明沒有辦法訪問到它了,因而可以將其占用的內(nèi)存空間回收。
除了一些極老版本的IE,目前市面上的JS引擎基本采用標記清除來除了垃圾回收。但是需要注意的是IE中的DOM由于機制問題,是采用了引用計數(shù)的方式,所以會有循環(huán)引用的問題,造成內(nèi)存泄露。
var ele = document.getElementById(“element”); var obj = new Object(); ele.obj = obj; // DOM元素ele的obj引用obj變量 obj.ele = ele; // obj變量的ele引用了DOM元素ele
這樣就造成了循環(huán)引用的問題,導致垃圾回收機制回收不了ele和obj。不過,可以在不使用ele和obj時,對這兩個變量進行 null 賦值,然后垃圾回收機制就會回收它們了。
3. 理解閉包在第2部分講解了三個重要的概念,這三個概念有助于我們更好的理解閉包。
我們再次拿出上面的這個例子:
function create(){ var i=0; // 返回一個函數(shù),暫且稱之為函數(shù)A return function(){ i++; console.log(i); } } var c = create(); // c是一個函數(shù),即函數(shù)A c(); // 函數(shù)執(zhí)行 c(); // 再次執(zhí)行 c(); // 第三次執(zhí)行
從上面的“每個函數(shù)都有自己的執(zhí)行環(huán)境”可以知道:create()函數(shù)是一個執(zhí)行環(huán)境,函數(shù)A也是一個執(zhí)行環(huán)境,且函數(shù)A的執(zhí)行環(huán)境在create()的里面。這樣就形成了一個作用域鏈:window->create->A。當執(zhí)行c()時,函數(shù)A就會首先在當前執(zhí)行環(huán)境中尋找變量i,可是沒有找到,那么只能順著作用域鏈向后找;OK,在create()的執(zhí)行環(huán)境中找到了,那么就可以使用了變量i了。
可是我們還有一個疑問,按照上面的說法,函數(shù)create()執(zhí)行完畢后,這個函數(shù)與里面的變量和方法應該被銷毀了呀,可是為什么函數(shù)c()多次執(zhí)行時依然能夠輸出變量i呢。這就是閉包的獨特之處。
函數(shù)create()執(zhí)行完畢后,雖然它的作用域鏈會被銷毀,即不再存在window->create這個鏈式關(guān)系,但是函數(shù)A()[c()]的作用域鏈還依然引用著create()的變量對象,還存在著window->create->A的鏈式關(guān)系,導致垃圾回收機制不能回收create()的變量對象,create()的變量對象仍然停留在內(nèi)存中,直到函數(shù)A()[c()]被銷毀后,create()的變量對象才會被銷毀。
因此,雖然create()已經(jīng)執(zhí)行完畢了,但是create()的變量對象并沒有被回收,還停留在內(nèi)存中,依然可以使用。
從上面的講解中我們可以看到,閉包會攜帶包含它的函數(shù)的作用域,因此會比其他函數(shù)占用更多的內(nèi)存。當頁面中存在過多的閉包,或者閉包的嵌套很多很深時,會導致內(nèi)存占用過多。因此,在這里建議:慎用閉包。
4. 閉包與變量有很多新手為DOM元素綁定事件時,通常會這么寫:
function bindClick(){ var li = document.getElementsByTagName("li"); // 假設一共有5個li標簽 for(var i=0; i
他的本意是想為每個li標簽綁定一個多帶帶的事件,點擊第幾個li標簽,就能輸出幾??墒?,最后的結(jié)果卻是,點擊哪個li標簽輸出的都是5,這是為什么呢?
其實這位程序員寫的bindClick()已經(jīng)構(gòu)成了一個閉包,下面的這個函數(shù)有他的作用域,而變量i本不屬于這個函數(shù)的作用域,而是屬于bindClick()中的:
// 匿名函數(shù) function(){ console.log("click the "+i+" li tag"); }
因此,這就構(gòu)成了一個含有閉包的作用域鏈:window->bindClick->匿名函數(shù)??墒沁@跟輸出的i有關(guān)系么?有。作用域鏈中的每個變量對象保存的是對變量和方法的引用,而不是保存這個變量的某一個值。當執(zhí)行到匿名函數(shù)時,bindClick()其實已經(jīng)執(zhí)行完畢了,變量i的值就是5,此時每個匿名函數(shù)都引用著同一個變量i。
不過我們稍微修改一下,以滿足我們的預期:
/* // 錯誤,onclick綁定的是立即執(zhí)行函數(shù)的返回值, // 而此立即執(zhí)行并沒有返回值,也就是onclick = undefined function bindClick(){ var li = document.getElementsByTagName("li"); for(var i=0; i
在這里,我們使用立即執(zhí)行的匿名函數(shù)來保證傳入的值就是當前正在操作的變量i,而不是循環(huán)完成后的值。
5. 閉包的應用場景(1)在內(nèi)存中維持一個變量。比如前面講的小例子,由于閉包,函數(shù)create()中的變量i會一直存在于內(nèi)存中,因此每次執(zhí)行c(),都會給變量i加1.
(2)保護函數(shù)內(nèi)的變量安全。還是那個小例子,函數(shù)create()中的變量c只有內(nèi)部的函數(shù)才能訪問,而無法通過其他途徑訪問到,因此保護了變量c的安全。
(3)實現(xiàn)面向?qū)ο笾械膶ο?。javascript并沒有提供類這樣的機制,但是我們可以通過閉包來模擬出類的機制,不同的對象實例擁有獨立的成員和狀態(tài)。
這里我們看一個例子:
function Student(){ var name = "wenzi"; return { setName : function(na){ name = na; }, getName : function(){ return name; } } } var stu = new Student(); console.log(stu.name); // undefined console.log(stu.getName()); // wenzi
這就是一個用閉包實現(xiàn)的簡單的類,里面的name屬性是私有的,外部無法進行訪問,只能通過setName和getName進行訪問。
當然,閉包還存在另外一種形式:
var a = (function(){ var num = 0; return function(){ return num++; } })() a(); // 0 a(); // 1 a(); // 2
本人個人博客:http://www.xiabingbao.com
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/85723.html
摘要:閉包引起的內(nèi)存泄漏總結(jié)從理論的角度將由于作用域鏈的特性中所有函數(shù)都是閉包但是從應用的角度來說只有當函數(shù)以返回值返回或者當函數(shù)以參數(shù)形式使用或者當函數(shù)中自由變量在函數(shù)外被引用時才能成為明確意義上的閉包。 文章同步到github js的閉包概念幾乎是任何面試官都會問的問題,最近把閉包這塊的概念梳理了一下,記錄成以下文章。 什么是閉包 我先列出一些官方及經(jīng)典書籍等書中給出的概念,這些概念雖然...
摘要:當在中調(diào)用匿名函數(shù)時,它們用的都是同一個閉包,而且在這個閉包中使用了和的當前值的值為因為循環(huán)已經(jīng)結(jié)束,的值為。最好將閉包當作是一個函數(shù)的入口創(chuàng)建的,而局部變量是被添加進這個閉包的。 閉包不是魔法 這篇文章使用一些簡單的代碼例子來解釋JavaScript閉包的概念,即使新手也可以輕松參透閉包的含義。 其實只要理解了核心概念,閉包并不是那么的難于理解。但是,網(wǎng)上充斥了太多學術(shù)性的文章,對于...
摘要:一言以蔽之,閉包,你就得掌握。當函數(shù)記住并訪問所在的詞法作用域,閉包就產(chǎn)生了。所以閉包才會得以實現(xiàn)。從技術(shù)上講,這就是閉包。執(zhí)行后,他的內(nèi)部作用域并不會消失,函數(shù)依然保持有作用域的閉包。 網(wǎng)上總結(jié)閉包的文章已經(jīng)爛大街了,不敢說筆者這篇文章多么多么xxx,只是個人理解總結(jié)。各位看官瞅瞅就好,大神還希望多多指正。此篇文章總結(jié)與《JavaScript忍者秘籍》 《你不知道的JavaScri...
摘要:權(quán)威指南第版中閉包的定義函數(shù)對象可以通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),這種特性在計算機科學文獻中成為閉包。循環(huán)中的閉包使用閉包時一種常見的錯誤情況是循環(huán)中的閉包,很多初學者都遇到了這個問題。 閉包簡介 閉包是JavaScript的重要特性,那么什么是閉包? 《JavaScript高級程序設計(第3版)》中閉包的定義: 閉包就是指有權(quán)訪問另一個函數(shù)中的變...
摘要:注意由于閉包會額外的附帶函數(shù)的作用域內(nèi)部匿名函數(shù)攜帶外部函數(shù)的作用域,因此,閉包會比其它函數(shù)多占用些內(nèi)存空間,過度的使用可能會導致內(nèi)存占用的增加。 作用域和作用域鏈是javascript中非常重要的特性,對于他們的理解直接關(guān)系到對于整個javascript體系的理解,而閉包又是對作用域的延伸,也是在實際開發(fā)中經(jīng)常使用的一個特性,實際上,不僅僅是javascript,在很多語言中都...
摘要:也許最好的理解是閉包總是在進入某個函數(shù)的時候被創(chuàng)建,而局部變量是被加入到這個閉包中。在函數(shù)內(nèi)部的函數(shù)的內(nèi)部聲明函數(shù)是可以的可以獲得不止一個層級的閉包。 前言 總括 :這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請自行忽略。 譯者 :文章寫在2006年,可直到翻譯的21小時之前作者還在完善這篇文章,在Stackoverflow的How do Java...
閱讀 1687·2019-08-29 13:53
閱讀 3276·2019-08-29 13:50
閱讀 924·2019-08-27 10:51
閱讀 653·2019-08-26 18:36
閱讀 1958·2019-08-26 11:00
閱讀 683·2019-08-26 10:36
閱讀 3290·2019-08-23 17:58
閱讀 2089·2019-08-23 15:17