關於js閉包是否真的會造成內存泄漏?

在很多書上,以及網上都可以看到關於內存泄漏的描述,一般都不太推薦使用閉包,那麼閉包使用多了,是不是真的會造成內存泄漏呢?


閉包不會造成內存泄漏。

程序寫錯了才會造成內存泄漏。

另外,那些書講的事情應該是:老瀏覽器(主要是IE6)由於垃圾回收有問題導致很容易出現內存泄漏。但是那是瀏覽器實現的bug。如果書裡面沒把這個意思寫清楚,那就是本爛書,不要看了,趕緊扔掉。


有沒有內存泄露,理解這個問題很簡單,自己寫個 test case。

  1. 將 test case 里寫入內存泄露的代碼,將所需內存池比例放大。
  2. 用 setTimeout 遞歸以上程序。
  3. 觀察內存的上漲情況。
  4. 最小化窗口,或者用 CollectGarbage 方法,測試 GC 回收是否有影響。

為什麼要用 setTimeout,是要看到過程,可分析。5 年前我寫這個 test case 的時候 IE 已有補丁解決該內存泄露的問題。(至少已沒有非常明顯的內存泄漏)


你寫的每10句js就有一句是閉包。。。最不濟你還有個全局閉包吧。。

另外:

很多人都會把內存使用和內存泄露搞混。用幾個變數不會被回收就內存泄露,你的內存就那麼少。。。所謂內存泄露是上個世紀某些瀏覽器的bug,跟js本身沒有關係。

不可盡信書

PS:在使用局部變數的時候不用手動=null來回收局部變數,在變數不會被引用到的時候,變數自然會被自動回收。其實最重要的就是理解,什麼樣的變數是不會被引用到的?理解了這個才是真正理解了閉包。


首先,如其他回答所言,IE6時代有bug,閉包會造成內存泄漏,這個現在已經無須考慮了。

其次,閉包本身不會造成內存泄漏,但閉包過多很容易導致內存泄漏。

這句話很矛盾,技術上講,閉包是不會造成內存泄漏的,瀏覽器的bug除外。但是,閉包會造成對象引用的生命周期脫離當前函數的上下文,因此,如果不仔細考慮閉包函數的生命周期,的確有可能出現意料之外的內存泄漏,當然,從嚴格意義上講,這是程序員自己的bug,而不是閉包的錯。

對於初學者而言,審慎的考量你的閉包函數的生命周期,確保沒有意料之外的對象引用殘留在脫離你的管理之外的閉包函數中,這也是一個很大的挑戰,但是幸運的是,這種情況其實對於大多數初學者而言並不容易出現,就算偶有發生,那點泄漏量現在的電腦一般還是經得住折騰的,所以,在初學階段,理解好閉包的原理,就可以。

PS,我最近寫那個框架,先是暴走了一周就出了個功能預覽版,顯然,內部必然大量使用閉包,而我又沒有時間去仔細考慮生命周期,基本就是隨用隨創建,創建完了還到處傳遞,是的,我把一個閉包函數到處傳遞,完全不關心它的生命周期,我都不用去開瀏覽器測試,自己用眼睛掃一遍代碼就能發現內存側漏一堆,從這個意義上講,閉包的確是會導致內存泄漏的。。。再然後,為了封堵側漏,我把整個代碼完全重寫了一遍,再然後,發現還是有脫離管理的幽靈引用一大堆,於是,我又重寫了第三遍,基本上模仿c程序的規範,創建指針,管理指針,釋放指針。。。寫到快完成的時候,我基本上邊寫邊吐,真的,你們試試把超過2000行的燒腦邏輯用完全不同的思路寫三遍試試。。。

照例,放廣告:

astamuse/asta4js · GitHub


執行下面這段代碼

var theThing = null;

function LeakClass() {
}

function MakeLeakObjects(count) {
var arr = [];
for (var i = 0; i &< count; ++i) { arr.push(new LeakClass()); } return arr; } var replaceThing = function () { var unused = function () { if (originalThing) console.log("hi"); }; var originalThing = theThing; theThing = { leaks: MakeLeakObjects(1000), someMethod: function () { } }; }; setInterval(replaceThing, 1000);

然後profiles -&> Take heap snapshot

按Object Count倒序排列,觀察LeakClass的內存佔用情況

參考

An interesting kind of JavaScript memory leak

Grokking V8 closures for fun (and profit?)


Javascript 的垃圾回收機制,我現在知道的有兩種:標記、計數。

  • 標記清除:主流策略,並且與此問題無關。
  • 引用計數:容易在循環引用時出現問題的策略。因為計數記錄的是被引用的次數,所以循環引用時計數並不會消除。導致無法釋放內存。IE 9 - 的問題是在環境中不僅有 DOM,還有 COM,COM 採用的就是計數回收機制。

我不確定閉包是不是會消除記數,因為當引用一個值(也就是函數值)的變數又取得另外一個值的時候(似乎閉包的每次循環都在重新賦值),引用記數就會降低。

但如果你也不確定,可以在閉包失去作用的作用域中手動斷開循環引用,也就是將變數賦值為 null。


只要你能保證所有與閉包變數 bind 的function 或object 執行或釋放掉,那絕對不會內存泄露

問題是做不到,也沒必要,發現泄露了,找到位置決掉,一般都是代碼姿勢不正確


看到這個問題正好我也想了解一下 就自己理順一下這個問題給我自己看看:)

首先要分析一下什麼是內存泄漏

我個人理解就是,程序會為本程序當中所需要的變數等 在內存中開闢一塊區域進行存儲,使用完畢後會進行釋放。內存泄漏就是程序並沒有處理好這一過程,導致程序運行完後並沒有合適的回收所佔用的空間。導致內存不斷的佔用,運行時間越長佔用的就越多,甚至佔滿內存。

網上查了一下內存泄漏大概分幾種

常發性

發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。

偶發性

發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。

一次性

發生內存泄漏的代碼只會被執行一次,或者由於演算法上的缺陷,導致總會有一塊且僅一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,所以內存泄漏只會發生一次。

隱式

程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這裡並沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但是對於一個伺服器程序,需要運行幾天、幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏為隱式內存泄漏。

然後再分析一下閉包

(網搜)在理解閉包以前.最好能先理解一下作用域鏈的含義,簡單來說,作用域鏈就是函數在定義的時候創建的,用於尋找使用到的變數的值的一個索引,而他內部的規則是,把函數自身的本地變數放在最前面,把自身的父級函數中的變數放在其次,把再高一級函數中的變數放在更後面,以此類推直至全局對象為止.當函數中需要查詢一個變數的值的時候,js解釋器會去作用域鏈去查找,從最前面的本地變數中先找,如果沒有找到對應的變數,則到下一級的鏈上找,一旦找到了變數,則不再繼續.如果找到最後也沒找到需要的變數,則解釋器返回undefined.

我們再來看看js的內存回收機制,一般來說,一個函數在執行開始的時候,會給其中定義的變數劃分內存空間保存,以備後面的語句所用,等到函數執行完畢返回了,這些變數就被認為是無用的了.對應的內存空間也就被回收了.下次再執行此函數的時候,所有的變數又回到最初的狀態,重新賦值使用.但是如果這個函數內部又嵌套了另一個函數,而這個函數是有可能在外部被調用到的.並且這個內部函數又使用了外部函數的某些變數的話.這種內存回收機制就會出現問題.如果在外部函數返回後,又直接調用了內部函數,那麼內部函數就無法讀取到他所需要的外部函數中變數的值了.所以js解釋器在遇到函數定義的時候,會自動把函數和他可能使用的變數(包括本地變數和父級和祖先級函數的變數(自由變數))一起保存起來.也就是構建一個閉包,這些變數將不會被內存回收器所回收,只有當內部的函數不可能被調用以後(例如被刪除了,或者沒有了指針),才會銷毀這個閉包,而沒有任何一個閉包引用的變數才會被下一次內存回收啟動時所回收.

如果真正理解這些概念 也可以使用閉包,閉包也是js當中非常有用的特性。js當中到處都有閉包, 定時器 事件監聽器 Ajax請求 跨窗口通信 web workers 或者任何非同步或者同步的任務中使用了回調 就是在使用閉包。但是 使用不好邏輯上處理不好會導致內存所謂的泄漏問題。

在使用閉包時候注意幾個問題吧:(網上搜的)

1)由於閉包會使得函數中的變數都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變數全部刪除。

2)閉包會在父函數外部,改變父函數內部變數的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便

改變父函數內部變數的值。


可能性:true

充分性:false

必要性:false


在低版本ie下dom是com,它採用的垃圾回收機制是引用計數,如果在閉包中引用了dom那麼它的引用計數會一直大於0,會一直無法回收,這就是閉包導致內存泄露的特殊情況吧,提一下,我們不用這個元素之後將其手動釋放就好。


你不知道的Javascript


閉包是一個非常強大的特性,但人們對其也有諸多無解。一種危言聳聽的說法是閉包會造成內存泄露。

局部變數本來應該在函數退出的時候被解除引用,但如果局部變數被封閉在閉包形成的環境中,那麼這個局部變數就能一直生存下去。從這個意義上看,閉包的確會使一些數據無法被及時銷毀。使用閉包的一部分原因是我們選擇主動把一些變數封存在閉包中,因為可能在以後還需要使用這些變數,把這些變數放在閉包中和放在全局作用域,對內存方面的影響是一致的,這裡並不能說成是內存泄露。如果在將來需要回收這些變數,我們可以手動把這些變數設為null。

跟閉包和內存泄露有關係的地方是,使用閉包的同時比較容易形成循環引用,如果閉包的作用域鏈中保存著一些DOM節點,這時候就有可能造成內存泄露。但這本身並非閉包的問題,也並非JavaScript的問題。在IE瀏覽器中,由於BOM和DOM中的對象是使用C++以COM對象的方式實現的,而COM對象的垃圾收集機制採用的是引用計數策略。在基於引用計數策略的垃圾回收機制中,如果兩個對象之間形成了循環引用,那麼這兩個對象都無法被回收,但循環引用造成的內存泄露在本質上也不是閉包造成的。

同樣,如果要解決循環引用帶來的內存泄露問題,我們只需要把循環引用中的變數設為null即可。將變數設置為null意味著切斷變數與它此前引用的值之間的聯繫。當垃圾收集器下次運行時,就會刪除這些值並回收他們佔用的內存。


其實我也不太搞得懂內存泄露,也沒遇到過類似的問題,不過如果那本書真的寫「不推薦使用閉包」,那完全就是扯淡了。至少現在公司對jser的要求絕對是掌握閉包比掌握內存泄漏要重要得多……

送題主一篇文章,內存管理 - JavaScript

從文章上看,事件綁定好像比較容易出問題。事件用完了記得清理,或者委託應該就ok了

還有個是循環引用,這例子看著挺極端的,而且也說到循環引用對現代瀏覽器也不是問題了,代碼好看點也應該不會出問題,可以用一些QA工具比如JSHint,JSLint之類的,注意一下函數的複雜度之類……

最重要的這個閉包和內存泄漏的問題,我自己也很困惑,閉包一般是為了創建一個獨立的作用域,然後會留一個外部可調用的介面,這樣的話,本身就不是可垃圾回收的吧。幹嘛還要和內存泄漏扯在一起?

如果程序真是因為閉包太多被搞到崩潰,我覺得應該是用模塊化,依賴管理的方面去結局,去搞內存泄漏有冇用?

求高人拍磚賜教

另外現在前端開發都有一整套輪子,照著輪子的模式做,應該不會內存泄漏吧。


扯淡的吧,閉包就是一個對象,要是頻繁創造相同的功能的對象,就得用原型來實現。

閉包嵌套過深,邏輯混亂,性能確實不會太好,除非你明白為啥這麼套,否則,實現的越簡單越好。


推薦閱讀:

前端工程師和網頁重構工程師二者有什麼區別和聯繫?
[1].slice.call({ length: 1, 0: 3 }) 為什麼返回[3]?
本人前端,剛入手了mac本,以前沒用過,請各位大大推薦一下mac本上做前端的編碼開發或者調試輔助工具?
哪裡有比較成熟的 React.js 項目案例?
960px 寬度的網格布局過時了嗎?

TAG:互聯網 | 前端開發 | 程序員 | JavaScript | 前端工程師 |