了解閉包 作用域鏈 垃圾回收機制

1. 什么是閉包?

來看一些關(guān)于閉包的定義:

閉包是指有權(quán)訪問另一個函數(shù)作用域中變量的函數(shù) --《JS高級程序設(shè)計第三版》 p178

函數(shù)對象可以通過作用域鏈相關(guān)聯(lián)起來,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),這種特性稱為 ‘閉包’ 。 --《JS權(quán)威指南》 p183

內(nèi)部函數(shù)可以訪問定義它們的外部函數(shù)的參數(shù)和變量(除了this和arguments)。 --《JS語言精粹》 p36

來個定義總結(jié)

可以訪問外部函數(shù)作用域中變量的函數(shù)

被內(nèi)部函數(shù)訪問的外部函數(shù)的變量可以保存在外部函數(shù)作用域內(nèi)而不被回收---這是核心,后面我們遇到閉包都要想到,我們要重點關(guān)注被閉包引用的這個變量。

來創(chuàng)建個簡單的閉包

來解讀后面兩個語句:

var say = sayName()?:返回了一個匿名的內(nèi)部函數(shù)保存在變量say中,并且引用了外部函數(shù)的變量name,由于垃圾回收機制,sayName函數(shù)執(zhí)行完畢后,變量name并沒有被銷毀。

say()?:執(zhí)行返回的內(nèi)部函數(shù),依然能訪問變量name,輸出 'jozo' .

2. 閉包中的作用域鏈

理解作用域鏈對理解閉包也很有幫助。

變量在作用域中的查找方式應(yīng)該都很熟悉了,其實這就是順著作用域鏈往上查找的。

當(dāng)函數(shù)被調(diào)用時:

先創(chuàng)建一個執(zhí)行環(huán)境(execution context),及相應(yīng)的作用域鏈;

將arguments和其他命名參數(shù)的值添加到函數(shù)的活動對象(activation object)

作用域鏈:當(dāng)前函數(shù)的活動對象優(yōu)先級最高,外部函數(shù)的活動對象次之,外部函數(shù)的外部函數(shù)的活動對象依次遞減,直至作用域鏈的末端--全局作用域。優(yōu)先級就是變量查找的先后順序;

先來看個普通的作用域鏈:

這段代碼包含兩個作用域:a.全局作用域;b.sayName函數(shù)的作用域,也就是只有兩個變量對象,當(dāng)執(zhí)行到對應(yīng)的執(zhí)行環(huán)境時,該變量對象會成為活動對象,并被推入到執(zhí)行環(huán)境作用域鏈的前端,也就是成為優(yōu)先級最高的那個。 看圖說話:

這圖在JS高級程序設(shè)計書上也有,我重新繪了遍。

在創(chuàng)建sayName()函數(shù)時,會創(chuàng)建一個預(yù)先包含變量對象的作用域鏈,也就是圖中索引為1的作用域鏈,并且被保存到內(nèi)部的[[Scope]]屬性中,當(dāng)調(diào)用sayName()函數(shù)的時候,會創(chuàng)建一個執(zhí)行環(huán)境,然后通過復(fù)制函數(shù)的[[Scope]]屬性中的對象構(gòu)建起作用域鏈,此后,又有一個活動對象(圖中索引為0)被創(chuàng)建,并被推入執(zhí)行環(huán)境作用域鏈的前端。

一般來說,當(dāng)函數(shù)執(zhí)行完畢后,局部活動對象就會被銷毀,內(nèi)存中僅保存全局作用域。但是,閉包的情況又有所不同 :

再來看看看閉包的作用域鏈:

這個閉包實例比上一個例子多了一個匿名函數(shù)的作用域:

在匿名函數(shù)從sayName()函數(shù)中被返回后,它的作用域鏈被初始化為包含sayName()函數(shù)的活動對象和全局變量對象。這樣,匿名函數(shù)就可以訪問在sayName()中定義的所有變量和參數(shù),更為重要的是,sayName()函數(shù)在執(zhí)行完畢后,其活動對象也不會被銷毀,因為匿名函數(shù)的作用域鏈依然在引用這個活動對象,換句話說,sayName()函數(shù)執(zhí)行完后,其執(zhí)行環(huán)境的作用域鏈會被銷毀,但他的活動對象會留在內(nèi)存中,知道匿名函數(shù)會銷毀。這個也是后面要講到的內(nèi)存泄露的問題。

作用域鏈問題不寫那么多了,寫書上的東西也很累 o(╯□╰)o

3. 閉包的實例

實例1:實現(xiàn)累加

實例2 :給每個li添加點擊事件

上面是一個經(jīng)典的例子,我們都知道執(zhí)行結(jié)果是都彈出5,也知道可以用閉包解決這個問題,但是我剛開始始終不能明白為什么每次彈出都是5,為什么閉包可以解決這問題。后來捋一捋還是把它弄清晰了:

a.先來分析沒用閉包前的情況:for循環(huán)中,我們給每個li點擊事件綁定了一個匿名函數(shù),匿名函數(shù)中返回了變量i的值,當(dāng)循環(huán)結(jié)束后,變量i的值變?yōu)?,此時我們再去點擊每個li,也就是執(zhí)行相應(yīng)的匿名函數(shù)(看上面的代碼),這是變量i已經(jīng)是5了,所以每個點擊彈出5. 因為這里返回的每個匿名函數(shù)都是引用了同一個變量i,如果我們新建一個變量保存循環(huán)執(zhí)行時當(dāng)前的i的值,然后再讓匿名函數(shù)應(yīng)用這個變量,最后再返回這個匿名函數(shù),這樣就可以達(dá)到我們的目的了,這就是運用閉包來實現(xiàn)的!

b.再來分析下運用閉包時的情況:

這里for循環(huán)執(zhí)行時,給點擊事件綁定的匿名函數(shù)傳遞i后立即執(zhí)行返回一個內(nèi)部的匿名函數(shù),因為參數(shù)是按值傳遞的,所以此時形參num保存的就是當(dāng)前i的值,然后賦值給局部變量 a,然后這個內(nèi)部的匿名函數(shù)一直保存著a的引用,也就是一直保存著當(dāng)前i的值。 所以循環(huán)執(zhí)行完畢后點擊每個li,返回的匿名函數(shù)執(zhí)行彈出各自保存的 a 的引用的值。

4. 閉包的運用

我們來看看閉包的用途。事實上,通過使用閉包,我們可以做很多事情。比如模擬面向?qū)ο蟮拇a風(fēng)格;更優(yōu)雅,更簡潔的表達(dá)出代碼;在某些方面提升代碼的執(zhí)行效率。

1. 匿名自執(zhí)行函數(shù)

我們在實際情況下經(jīng)常遇到這樣一種情況,即有的函數(shù)只需要執(zhí)行一次,其內(nèi)部變量無需維護(hù),比如UI的初始化,那么我們可以使用閉包:

//將全部li字體變?yōu)榧t色(function(){varels =document.getElementsByTagName('li');for(vari =0,lng = els.length;i < lng;i++){? ? ? ? els[i].style.color ='red';? ? }? ? })();

我們創(chuàng)建了一個匿名的函數(shù),并立即執(zhí)行它,由于外部無法引用它內(nèi)部的變量,

因此els,i,lng這些局部變量在執(zhí)行完后很快就會被釋放,節(jié)省內(nèi)存!

關(guān)鍵是這種機制不會污染全局對象。

2. 實現(xiàn)封裝/模塊化代碼

3. 實現(xiàn)面向?qū)ο笾械膶ο?/b>

這樣不同的對象(類的實例)擁有獨立的成員及狀態(tài),互不干涉。雖然JavaScript中沒有類這樣的機制,但是通過使用閉包,

我們可以模擬出這樣的機制。還是以上邊的例子來講:

Person的兩個實例person1 和 person2 互不干擾!因為這兩個實例對name這個成員的訪問是獨立的 。

5. 內(nèi)存泄露及解決方案

垃圾回收機制

說到內(nèi)存管理,自然離不開JS中的垃圾回收機制,有兩種策略來實現(xiàn)垃圾回收:標(biāo)記清除 和 引用計數(shù);

標(biāo)記清除:垃圾收集器在運行的時候會給存儲在內(nèi)存中的所有變量都加上標(biāo)記,然后,它會去掉環(huán)境中的變量的標(biāo)記和被環(huán)境中的變量引用的變量的標(biāo)記,此后,如果變量再被標(biāo)記則表示此變量準(zhǔn)備被刪除。 2008年為止,IE,F(xiàn)irefox,opera,chrome,Safari的javascript都用使用了該方式;

引用計數(shù):跟蹤記錄每個值被引用的次數(shù),當(dāng)聲明一個變量并將一個引用類型的值賦給該變量時,這個值的引用次數(shù)就是1,如果這個值再被賦值給另一個變量,則引用次數(shù)加1。相反,如果一個變量脫離了該值的引用,則該值引用次數(shù)減1,當(dāng)次數(shù)為0時,就會等待垃圾收集器的回收。

這個方式存在一個比較大的問題就是循環(huán)引用,就是說A對象包含一個指向B的指針,對象B也包含一個指向A的引用。 這就可能造成大量內(nèi)存得不到回收(內(nèi)存泄露),因為它們的引用次數(shù)永遠(yuǎn)不可能是 0 。早期的IE版本里(ie4-ie6)采用是計數(shù)的垃圾回收機制,閉包導(dǎo)致內(nèi)存泄露的一個原因就是這個算法的一個缺陷。

我們知道,IE中有一部分對象并不是原生額javascript對象,例如,BOM和DOM中的對象就是以COM對象的形式實現(xiàn)的,而COM對象的垃圾回收機制采用的就是引用計數(shù)。因此,雖然IE的javascript引擎采用的是標(biāo)記清除策略,但是訪問COM對象依然是基于引用計數(shù)的,因此只要在IE中設(shè)計COM對象就會存在循環(huán)引用的問題!

舉個栗子:

window.onload = function(){

? ? var el = document.getElementById("id");

? ? el.onclick = function(){

? ? ? ? alert(el.id);

? ? }

}

這段代碼為什么會造成內(nèi)存泄露?

el.onclick= function () {

? ? alert(el.id);

};

執(zhí)行這段代碼的時候,將匿名函數(shù)對象賦值給el的onclick屬性;然后匿名函數(shù)內(nèi)部又引用了el對象,存在循環(huán)引用,所以不能被回收;

解決方法:

window.onload = function(){

? ? var el = document.getElementById("id");

? ? var id = el.id; //解除循環(huán)引用

? ? el.onclick = function(){

? ? ? ? alert(id);

? ? }

? ? el = null; // 將閉包引用的外部函數(shù)中活動對象清除

}

6. 總結(jié)閉包的優(yōu)缺點

優(yōu)點:

可以讓一個變量常駐內(nèi)存 (如果用的多了就成了缺點

避免全局變量的污染

私有化變量

缺點

因為閉包會攜帶包含它的函數(shù)的作用域,因此會比其他函數(shù)占用更多的內(nèi)存

引起內(nèi)存泄露

作者:你為什么無理取鬧

鏈接:http://www.itdecent.cn/p/4432d3ea6296

來源:簡書

簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容