Javascript 是Java家族中最受歡迎的成員,在該語(yǔ)言的開(kāi)發(fā)應(yīng)用過(guò)程中,其內(nèi)存泄漏是一個(gè)重要的問(wèn)題。中培偉業(yè)《企業(yè)級(jí)JAVA高級(jí)開(kāi)發(fā)技術(shù)實(shí)戰(zhàn)》培訓(xùn)專(zhuān)家劉老師在這里介紹了詳解4 種常見(jiàn)的 Javascript 內(nèi)存泄露問(wèn)題。
1: 意外的全局變量
Javascript 語(yǔ)言的設(shè)計(jì)目標(biāo)之一是開(kāi)發(fā)一種類(lèi)似于 Java 但是對(duì)初學(xué)者十分友好的語(yǔ)言。體現(xiàn) JavaScript 寬容性的一點(diǎn)表現(xiàn)在它處理未聲明變量的方式上:一個(gè)未聲明變量的引用會(huì)在全局對(duì)象中創(chuàng)建一個(gè)新的變量。
如果 bar 是一個(gè)應(yīng)該指向 foo 函數(shù)作用域內(nèi)變量的引用,但是你忘記使用 var 來(lái)聲明這個(gè)變量,這時(shí)一個(gè)全局變量就會(huì)被創(chuàng)建出來(lái)。在這個(gè)例子中,一個(gè)簡(jiǎn)單的字符串泄露并不會(huì)造成很大的危害,但這無(wú)疑是錯(cuò)誤的。
為了防止這種錯(cuò)誤的發(fā)生,可以在你的 JavaScript 文件開(kāi)頭添加 'use strict'; 語(yǔ)句。這個(gè)語(yǔ)句實(shí)際上開(kāi)啟了解釋 JavaScript 代碼的嚴(yán)格模式,這種模式可以避免創(chuàng)建意外的全局變量。
全局變量的注意事項(xiàng)
盡管我們?cè)谟懻撃切╇[蔽的全局變量,但是也有很多代碼被明確的全局變量污染的情況。按照定義來(lái)講,這些都是不會(huì)被回收的變量(除非設(shè)置 null 或者被重新賦值)。特別需要注意的是那些被用來(lái)臨時(shí)存儲(chǔ)和處理一些大量的信息的全局變量。如果你必須使用全局變量來(lái)存儲(chǔ)很多的數(shù)據(jù),請(qǐng)確保在使用過(guò)后將它設(shè)置為 null 或者將它重新賦值。
常見(jiàn)的和全局變量相關(guān)的引發(fā)內(nèi)存消耗增長(zhǎng)的原因就是緩存。緩存存儲(chǔ)著可復(fù)用的數(shù)據(jù)。為了讓這種做法更高效,必須為緩存的容量規(guī)定一個(gè)上界。由于緩存不能被及時(shí)回收的緣故,緩存無(wú)限制地增長(zhǎng)會(huì)導(dǎo)致很高的內(nèi)存消耗。
2: 被遺漏的定時(shí)器和回調(diào)函數(shù)
JavaScript 中 setInterval 的使用十分常見(jiàn)。其他的庫(kù)也經(jīng)常會(huì)提供觀察者和其他需要回調(diào)的功能。這些庫(kù)中的絕大部分都會(huì)關(guān)注一點(diǎn),就是當(dāng)它們本身的實(shí)例被銷(xiāo)毀之前銷(xiāo)毀所有指向回調(diào)的引用。
那些表示節(jié)點(diǎn)的對(duì)象在將來(lái)可能會(huì)被移除掉,所以將整個(gè)代碼塊放在周期處理函數(shù)中并不是必要的。然而,由于周期函數(shù)一直在運(yùn)行,處理函數(shù)并不會(huì)被回收(只有周期函數(shù)停止運(yùn)行之后才開(kāi)始回收內(nèi)存)。如果周期處理函數(shù)不能被回收,它的依賴程序也同樣無(wú)法被回收。這意味著一些資源,也許是一些相當(dāng)大的數(shù)據(jù)都也無(wú)法被回收。
以前在 IE 瀏覽器的垃圾回收器上會(huì)導(dǎo)致一個(gè) bug(或者說(shuō)是瀏覽器設(shè)計(jì)上的問(wèn)題)。舊版本的 IE 瀏覽器不會(huì)發(fā)現(xiàn) DOM 節(jié)點(diǎn)和 JavaScript 代碼之間的循環(huán)引用。這是一種觀察者的典型情況,觀察者通常保留著一個(gè)被觀察者的引用換句話說(shuō),在 IE 瀏覽器中,每當(dāng)一個(gè)觀察者被添加到一個(gè)節(jié)點(diǎn)上時(shí),就會(huì)發(fā)生一次內(nèi)存泄漏。這也就是開(kāi)發(fā)者在節(jié)點(diǎn)或者空的引用被添加到觀察者中之前顯式移除處理方法的原因。
目前,現(xiàn)代的瀏覽器(包括 IE 和 Microsoft Edge)都使用了可以發(fā)現(xiàn)這些循環(huán)引用并正確的處理它們的現(xiàn)代化垃圾回收算法。換言之,嚴(yán)格地講,在廢棄一個(gè)節(jié)點(diǎn)之前調(diào)用 removeEventListener 不再是必要的操作。
像是 jQuery 這樣的框架和庫(kù)(當(dāng)使用一些特定的 API 時(shí)候)都在廢棄一個(gè)結(jié)點(diǎn)之前移除了 listener 。它們?cè)趦?nèi)部就已經(jīng)處理了這些事情,并且保證不會(huì)產(chǎn)生內(nèi)存泄露,即便程序運(yùn)行在那些問(wèn)題很多的瀏覽器中,比如老版本的 IE。
3: DOM 之外的引用
有些情況下將 DOM 結(jié)點(diǎn)存儲(chǔ)到數(shù)據(jù)結(jié)構(gòu)中會(huì)十分有用。假設(shè)你想要快速地更新一個(gè)表格中的幾行,如果你把每一行的引用都存儲(chǔ)在一個(gè)字典或者數(shù)組里面會(huì)起到很大作用。如果你這么做了,程序中將會(huì)保留同一個(gè)結(jié)點(diǎn)的兩個(gè)引用:一個(gè)引用存在于 DOM 樹(shù)中,另一個(gè)被保留在字典中。如果在未來(lái)的某個(gè)時(shí)刻你決定要將這些行移除,則需要將所有的引用清除。
假設(shè)你在 JavaScript 代碼中保留了一個(gè)表格中特定單元格(一個(gè) <td> 標(biāo)簽)的引用。在將來(lái)你決定將這個(gè)表格從 DOM 中移除,但是仍舊保留這個(gè)單元格的引用。憑直覺(jué),你可能會(huì)認(rèn)為 GC 會(huì)回收除了這個(gè)單元格之外所有的東西,但是實(shí)際上這并不會(huì)發(fā)生:?jiǎn)卧袷潜砀竦囊粋€(gè)子節(jié)點(diǎn)且所有子節(jié)點(diǎn)都保留著它們父節(jié)點(diǎn)的引用。換句話說(shuō),JavaScript 代碼中對(duì)單元格的引用導(dǎo)致整個(gè)表格被保留在內(nèi)存中。所以當(dāng)你想要保留 DOM 元素的引用時(shí),要仔細(xì)的考慮清除這一點(diǎn)。
4: 閉包
JavaScript 開(kāi)發(fā)中一個(gè)重要的內(nèi)容就是閉包,它是可以獲取父級(jí)作用域的匿名函數(shù)。Meteor 的開(kāi)發(fā)者發(fā)現(xiàn)在一種特殊情況下有可能會(huì)以一種很微妙的方式產(chǎn)生內(nèi)存泄漏,這取決于 JavaScript 運(yùn)行時(shí)的實(shí)現(xiàn)細(xì)節(jié)。
本質(zhì)上來(lái)講,創(chuàng)建了一個(gè)閉包鏈表(根節(jié)點(diǎn)是 theThing 形式的變量),而且每個(gè)閉包作用域都持有一個(gè)對(duì)大數(shù)組的間接引用,這導(dǎo)致了一個(gè)巨大的內(nèi)存泄露。