理解閉包與內(nèi)存泄漏
一、閉包的定義
閉包,是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中變量的函數(shù)。從定義上我們可以知道,閉包是函數(shù),并且是被另一個(gè)函數(shù)包裹的函數(shù)。所以需要用一個(gè)函數(shù)去包裹另一個(gè)函數(shù),即在函數(shù)內(nèi)部定義函數(shù)。被包裹的函數(shù)則稱為閉包函數(shù),包裹的函數(shù)(外部的函數(shù))則為閉包函數(shù)提供了一個(gè)閉包作用域,所以形成的閉包作用域的名稱為外部函數(shù)的名稱。
我們先來(lái)看一個(gè)常見(jiàn)的閉包例子,如:
let?foo;
function?outer()?{?//?outer函數(shù)內(nèi)部為閉包函數(shù)提供一個(gè)閉包作用域(outer)
????let?bar?=?"bar";
????let?inner?=?function()?{
????????console.log(bar);
????????debugger;?//?打一個(gè)debuuger斷點(diǎn),以便查看閉包作用域
????????console.log("inner?function?run.");
????}
????return?inner;
}
foo?=?outer();?//?執(zhí)行外部函數(shù)返回內(nèi)部函數(shù)
foo();?//?執(zhí)行內(nèi)部函數(shù)
我們?cè)跒g覽器上執(zhí)行該段代碼后,會(huì)停在斷點(diǎn)位置,此時(shí)我們可以看到形成的閉包作用域如圖所示

從圖中我們可以看到,形成的閉包作用域名稱為外部的outer函數(shù)提供的作用域,閉包作用域內(nèi)有一個(gè)變量bar可以被閉包函數(shù)訪問(wèn)到。
二、形成閉包的條件
從上面的閉包例子在,看起來(lái)形成的閉包的條件就是,一個(gè)函數(shù)被另一個(gè)函數(shù)包裹,并且返回這個(gè)被包裹的函數(shù)供外部持有。其實(shí),閉包函數(shù)是否被外部變量持有并不重要,形成閉包的必要條件就是,閉包函數(shù)(被包裹的函數(shù))中必須要使用到外部函數(shù)中的變量。
function?outer()?{?//?outer函數(shù)內(nèi)部為閉包函數(shù)提供一個(gè)閉包作用域(outer)
????let?bar?=?"bar";
????let?inner?=?function()?{
????????console.log(bar);
????????debugger;
????????console.log("inner?function?run.");
????}
????inner();?//?直接在外部函數(shù)中執(zhí)行閉包函數(shù)inner
}
outer();
我們稍微修改一下上面的例子,外部函數(shù)outer不將內(nèi)部函數(shù)inner返回,而是直接在outer內(nèi)執(zhí)行。

從執(zhí)行結(jié)果可以看到,仍然形成了閉包,所以說(shuō)這個(gè)被包裹的閉包函數(shù)是否被外部持有并不是形成閉包的條件。
function?outer()?{?//?outer函數(shù)內(nèi)部為閉包函數(shù)提供一個(gè)閉包作用域(outer)
????let?bar?=?"bar";
????let?inner?=?function()?{
????????//?console.log(bar);?//?注釋該行,內(nèi)部inner函數(shù)不再使用外部outer函數(shù)中的變量
????????debugger;
????????console.log("inner?function?run.");
????}
????inner();?//?直接在外部函數(shù)中執(zhí)行閉包函數(shù)inner
}
outer();
我們?cè)傩薷囊幌律厦娴睦?,將console.log(bar)這行代碼注釋掉,這樣inner函數(shù)中將不再使用外部outer函數(shù)中的變量。

從執(zhí)行結(jié)果上可以看到,沒(méi)有形成閉包。所以形成閉包的必要條件就是,被包裹的閉包函數(shù)必須使用外部函數(shù)中的變量。
當(dāng)然上面的結(jié)論也太過(guò)絕對(duì)了些,因?yàn)?strong>外部函數(shù)可以同時(shí)包裹多個(gè)閉包函數(shù),也就是說(shuō),(外部)函數(shù)內(nèi)部定義了多個(gè)函數(shù),這種情況下,就不需要每個(gè)閉包函數(shù)都使用到外部函數(shù)中的變量,因?yàn)?strong>閉包作用域是內(nèi)部所有閉包函數(shù)共享的,只要有一個(gè)內(nèi)部函數(shù)使用到了外部函數(shù)中的變量即可形成閉包。
function?outer()?{?//?outer函數(shù)內(nèi)部為閉包函數(shù)提供一個(gè)閉包作用域(outer)
????let?bar?=?"bar";
????let?unused?=?function()?{
????????console.log(bar);?//?再創(chuàng)建一個(gè)閉包函數(shù),并在其中使用外部函數(shù)中的變量
????}
????let?inner?=?function()?{
????????//?console.log(bar);?//?注釋該行,內(nèi)部inner函數(shù)不再使用外部outer函數(shù)中的變量
????????debugger;
????????console.log("inner?function?run.");
????}
????inner();?//?直接在外部函數(shù)中執(zhí)行閉包函數(shù)inner
}
outer();
我們繼續(xù)修改一下上面的例子,在outer函數(shù)內(nèi)部再創(chuàng)建一個(gè)unused函數(shù),這個(gè)函數(shù)只是定義但不會(huì)執(zhí)行,同時(shí)unused函數(shù)內(nèi)部使用了外部outer函數(shù)中的變量,inner函數(shù)仍然不使用外部outer函數(shù)中的變量。

從執(zhí)行結(jié)果可以看到,又形成了閉包。所以形成的閉包條件就是,存在內(nèi)部函數(shù)中使用外部函數(shù)中定義的變量。
三、內(nèi)存泄漏
內(nèi)存泄漏常常與閉包緊緊聯(lián)系在一起,很容易讓人誤以為閉包就會(huì)導(dǎo)致內(nèi)存泄漏。其實(shí)閉包只是讓內(nèi)存常駐,而濫用閉包才會(huì)導(dǎo)致內(nèi)存泄漏。
內(nèi)存泄漏,從廣義上說(shuō)就是,內(nèi)存在使用完畢之后,對(duì)于不再要的內(nèi)存沒(méi)有及時(shí)釋放或者無(wú)法釋放。不再需要的內(nèi)存使用完畢之后肯定需要釋放掉,否則這個(gè)塊內(nèi)存就浪費(fèi)掉了,相當(dāng)于內(nèi)存泄漏了。但是在實(shí)際中,往往不會(huì)通過(guò)判斷該內(nèi)存或變量是否不再需要使用來(lái)判斷。因?yàn)閮?nèi)存測(cè)試工具很難判斷該內(nèi)存是否不再需要。所以我們通常會(huì)重復(fù)多次執(zhí)行某段邏輯鏈路,然后每隔一段時(shí)間進(jìn)行一次內(nèi)存dump,然后判斷內(nèi)存是否存在不斷增長(zhǎng)的趨勢(shì),如果存在,則可用懷疑存在內(nèi)存泄漏的可能。
四、內(nèi)存dump
瀏覽器中抓取內(nèi)存的dump相對(duì)來(lái)說(shuō)簡(jiǎn)單些,直接通過(guò)谷歌瀏覽器的調(diào)試工具找到memory對(duì)應(yīng)的tab頁(yè)面,然后點(diǎn)擊Load即可開(kāi)始抓取內(nèi)存dump,如:

在NodeJS中,我們也可以通過(guò)引入heapdump來(lái)抓取內(nèi)存dump,直接通過(guò)npm安裝heapdump模塊即可
>?npm?install?heapdump
安裝完成之后,即可直接在應(yīng)用程序中使用了,用法非常簡(jiǎn)單,如:
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
//?應(yīng)用code部分
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
應(yīng)用程序執(zhí)行完成后,會(huì)在應(yīng)用根目錄中生成start.heapsnapshot和end.heapsnapshot兩個(gè)內(nèi)存dump文件,我們可以通過(guò)判斷兩個(gè)文件的大小變化來(lái)判斷是否存在內(nèi)存泄漏。
當(dāng)然并不是說(shuō)內(nèi)存dump文件的大小不斷增大就存在內(nèi)存泄漏,如果應(yīng)用的訪問(wèn)量確實(shí)在一直增大,那么內(nèi)存曲線只增不減也屬于正常情況,我們只能根據(jù)具體情況判斷是否存在內(nèi)存泄漏的可能。
五、常見(jiàn)的內(nèi)存泄漏
① 閉包循環(huán)引用
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
let?foo?=?null;
function?outer()?{
????let?bar?=?foo;
????function?unused()?{?//?未使用到的函數(shù)
????????console.log(`bar?is?${bar}`);
????}
????foo?=?{?//?給foo變量重新賦值
????????bigData:?new?Array(100000).join("this_is_a_big_data"),?//?如果這個(gè)對(duì)象攜帶的數(shù)據(jù)非常大,將會(huì)造成非常大的內(nèi)存泄漏
????????inner:?function()?{
????????????console.log(`inner?method?run`);
????????}
????}
}
for(let?i?=?0;?i?1000;?i++)?{
????outer();
}
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
在這個(gè)例子中,執(zhí)行了1000次outer函數(shù),start.heapsnapshot文件的大小為2.4M,而end.heapsnapshot文件的大小為4.1M,所以可能存在內(nèi)存泄漏。
前面講解閉包的過(guò)程中,我們已經(jīng)可以知道outer函數(shù)內(nèi)部是存在閉包的,因?yàn)閛uter函數(shù)內(nèi)部定義了unused和inner兩個(gè)函數(shù),雖然inner函數(shù)中沒(méi)有使用到outer函數(shù)中的變量,但是unused函數(shù)內(nèi)部使用到了outer函數(shù)中的bar變量,故形成閉包,inner函數(shù)也會(huì)共享outer函數(shù)提供的閉包作用域。
由于閉包的存在,bar變量不能釋放,即相當(dāng)于inner函數(shù)隱式持有了bar變量,所以存在...-->foo-->inner-->bar-->foo(賦值給bar的foo,即上一次的foo)...。
這里inner隱式持有bar變量怎么理解呢?因?yàn)閕nner是一個(gè)閉包函數(shù),可以使用outer提供的閉包作用域中的bar變量,由于閉包的關(guān)系,bar變量不能釋放,所以bar變量一直在內(nèi)存中,而bar變量又指向了上一次賦值給bar的foo對(duì)象,所以會(huì)存在這樣一個(gè)引用關(guān)系。
那怎么解決呢?由于bar變量常駐內(nèi)存不能釋放,所以我們可以在outer函數(shù)執(zhí)行完畢的時(shí)候手動(dòng)釋放,即將bar變量置為null,這樣之前賦值給bar的foo對(duì)象就沒(méi)有被其他變量引用了,就會(huì)被回收了。
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
let?foo?=?null;
function?outer()?{
????let?bar?=?foo;
????function?unused()?{?//?未使用到的函數(shù)
????????console.log(`bar?is?${bar}`);
????}
????foo?=?{?//?給foo變量重新賦值
????????bigData:?new?Array(100000).join("this_is_a_big_data"),?//?如果這個(gè)對(duì)象攜帶的數(shù)據(jù)非常大,將會(huì)造成非常大的內(nèi)存泄漏
????????inner:?function()?{
????????????console.log(`inner?method?run`);
????????}
????}
????bar?=?null;?//?手動(dòng)釋放bar變量,解除bar變量對(duì)上一次foo對(duì)象的引用
}
for(let?i?=?0;?i?1000;?i++)?{
????outer();
}
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
手動(dòng)釋放bar變量是一種相對(duì)比較好的解決方式。關(guān)鍵在于要解除閉包解除bar變量對(duì)上一次foo變量的引用。所以我們可以讓unused方法內(nèi)不使用bar變量,或者將bar變量的定義放在一個(gè)塊級(jí)作用域中,如:
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
let?foo?=?null;
function?outer()?{
????{?//?將bar變量定義在一個(gè)塊級(jí)作用域內(nèi),這樣outer函數(shù)中就沒(méi)有定義變量了,自然inner也不會(huì)形成閉包
????????let?bar?=?foo;
????????function?unused()?{?//?未使用到的函數(shù)
????????????console.log(`bar?is?${bar}`);
????????}
????}
????foo?=?{?//?給foo變量重新賦值
????????bigData:?new?Array(100000).join("this_is_a_big_data"),?//?如果這個(gè)對(duì)象攜帶的數(shù)據(jù)非常大,將會(huì)造成非常大的內(nèi)存泄漏
????????inner:?function()?{
????????????console.log(`inner?method?run`);
????????}
????}
}
for(let?i?=?0;?i?1000;?i++)?{
????outer();
}
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
② 重復(fù)注冊(cè)事件,比如頁(yè)面一進(jìn)入就重復(fù)注冊(cè)1000個(gè)同名事件(一次模擬每次進(jìn)入頁(yè)面都注冊(cè)一次事件)
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
const?events?=?require('events');
class?Page?extends?events.EventEmitter?{
????onShow()?{
????????for?(let?i?=?0;?i?1000;?i++)?{
????????????this.on("ok",?()?=>?{
????????????????console.log("on?ok?signal.");
????????????});
????????}
????}
????onDestory()?{
????????
????}
}
let?page?=?new?Page();
page.setMaxListeners(0);?//?設(shè)置可以注冊(cè)多個(gè)同名事件
page.onShow();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
這個(gè)例子中Page頁(yè)面一進(jìn)入就會(huì)同時(shí)注冊(cè)1000個(gè)同名的ok事件,start.heapsnapshot文件的大小為2.4M,而end.heapsnapshot文件的大小為2.5M,所以可能存在內(nèi)存泄漏。
解決方式就是,在頁(yè)面離開(kāi)的時(shí)候移除所有事件,或者在頁(yè)面創(chuàng)建的時(shí)候僅注冊(cè)一次事件,如:
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
const?events?=?require('events');
class?Page?extends?events.EventEmitter?{
????onCreate()?{
????????this.on("ok",?()?=>?{?//?僅在頁(yè)面創(chuàng)建的時(shí)候注冊(cè)一次事件,避免重復(fù)注冊(cè)事件
????????????console.log("on?ok?signal.");
????????});
????}
????onShow()?{
????????//?for?(let?i?=?0;?i?1000;?i++)?{
????????//?????this.on("ok",?()?=>?{
????????//?????????console.log("on?ok?signal.");
????????//?????});
????????//?}
????}
????onLeave()?{
????????this.removeAllListeners("ok");?//?或者在離開(kāi)頁(yè)面的時(shí)候移除所有ok事件
????}
}
let?page?=?new?Page();
page.setMaxListeners(0);?//?設(shè)置可以注冊(cè)多個(gè)同名事件
page.onCreate();
page.onShow();
page.onLeave();
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
③ 意外的全局變量,這是我們常常簡(jiǎn)單的內(nèi)存泄漏例子,實(shí)際上內(nèi)存工具很難判斷意外的全局變量是否存在內(nèi)存泄漏,除非應(yīng)用程序不斷的往這個(gè)全局變量中加入數(shù)據(jù),否則對(duì)于一個(gè)恒定不變的意外全局變量?jī)?nèi)存測(cè)試工具是無(wú)法判斷出是否存在內(nèi)存泄漏的,所以我們盡量不要隨意使用全局變量來(lái)保存數(shù)據(jù)。
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
function?createBigData()?{
????const?bigData?=?[];
????for(let?j?=?0;?j?100;?j++)?{
????????bigData.push(new?Array(10000).join("this_is_a_big_data"));
????}
????return?bigData;
}
function?fn()?{
????foo?=?createBigData();?//?意外的全局變量導(dǎo)致內(nèi)存泄漏
}
for?(let?j?=?0;?j?100;?j++)?{
????fn();
}
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
該例子執(zhí)行后,end.heapsnapshot文件的大小為2.5M也變成了2.5M,執(zhí)行fn函數(shù)的時(shí)候意外產(chǎn)生了一個(gè)全局變量foo,并賦值為了一個(gè)很大的數(shù)據(jù),如果foo變量用完后我們不再需要,那么我們就要主動(dòng)釋放,否則常駐內(nèi)存造成內(nèi)存泄漏,如果這個(gè)全局變量我們后續(xù)還需要使用到,那么就不算內(nèi)存泄漏。
解決方法就是,將foo定義成局部變量,如:
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
function?createBigData()?{
????const?bigData?=?[];
????for(let?j?=?0;?j?100;?j++)?{
????????bigData.push(new?Array(10000).join("this_is_a_big_data"));
????}
????return?bigData;
}
function?fn()?{
????//?foo?=?createBigData();?//?意外的全局變量導(dǎo)致內(nèi)存泄漏
????const?foo?=?createBigData();?//?將foo定義為局部變量,避免內(nèi)存泄漏
}
for?(let?j?=?0;?j?100;?j++)?{
????fn();
}
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
④ 事件未及時(shí)銷毀
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
const?events?=?require('events');
function?createBigData()?{
????const?bigData?=?[];
????for(let?j?=?0;?j?100;?j++)?{
????????bigData.push(new?Array(100000).join("this_is_a_big_data"));
????}
????return?bigData;
}
class?Page?extends?events.EventEmitter?{
????onCreate()?{
????????const?data?=?createBigData();
????????this.handler?=?()?=>?{
????????????this.update(data);
????????}
????????this.on("ok",?this.handler);
????}
????update(data)?{
????????console.log("開(kāi)始更新數(shù)據(jù)了");?//?接收到ok信號(hào),可以開(kāi)始更新數(shù)據(jù)了
????}
????onDestory()?{
???????
????}
}
let?page?=?new?Page();
page.onCreate();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
此例中頁(yè)面onCreate的時(shí)候會(huì)注冊(cè)一個(gè)ok事件,事件處理函數(shù)為this.handler,this.handler的定義會(huì)形成一個(gè)閉包,導(dǎo)致data無(wú)法釋放,從而內(nèi)存溢出。
解決辦法就是移除事件并清空this.handler,因?yàn)閠his.handler這個(gè)閉包函數(shù)被兩個(gè)變量持有,一個(gè)是page對(duì)象的handler屬性持有,另一個(gè)是事件處理器由于注冊(cè)事件后被事件處理器所持有。所以需要釋放this.handler并且移除事件監(jiān)聽(tīng)。
const?heapdump?=?require('heapdump');
heapdump.writeSnapshot('start.heapsnapshot');?//?記錄應(yīng)用開(kāi)始時(shí)的內(nèi)存dump
const?events?=?require('events');
function?createBigData()?{
????const?bigData?=?[];
????for(let?j?=?0;?j?100;?j++)?{
????????bigData.push(new?Array(100000).join("this_is_a_big_data"));
????}
????return?bigData;
}
class?Page?extends?events.EventEmitter?{
????onCreate()?{
????????const?data?=?createBigData();
????????this.handler?=?()?=>?{
????????????this.update(data);
????????}
????????this.on("ok",?this.handler);
????}
????update(data)?{
????????console.log("開(kāi)始更新數(shù)據(jù)了");?//?接收到ok信號(hào),可以開(kāi)始更新數(shù)據(jù)了
????}
????onDestory()?{
????????this.removeListener("ok",?this.handler);?//?移除ok事件,解決事件處理器對(duì)this.handler閉包函數(shù)的引用
????????this.handler?=?null;?//解除page對(duì)象對(duì)this.handler閉包函數(shù)的引用
????}
}
let?page?=?new?Page();
page.onCreate();
page.onDestory();
heapdump.writeSnapshot('end.heapsnapshot');?//?記錄應(yīng)用結(jié)束時(shí)的內(nèi)存dump
解除page對(duì)象和事件處理器對(duì)象對(duì)this.handler閉包函數(shù)的引用后,this.handler閉包函數(shù)就會(huì)被釋放,從而解除閉包,data也會(huì)得到釋放。

