1.意外的全局變量
JavaScript的目標是開發壹種看起來像Java但足夠自由的被初學者使用的語言。JavaScript自由的其中壹種方式是它可以處理沒有聲明的變量:壹個未聲明的變量的引用在全局對象中創建了壹個新變量。在瀏覽器的環境中,全局對象是window。也就是說:
123
function foo(arg) {bar = "this is a hidden global variable";}
實際上是:
123
function foo(arg) {window.bar = "this is an explicit global variable";}
如果bar是僅在foo函數作用域內承載引用,並且妳忘記用var來聲明的變量,壹個意外的全局變量就被創建了。在這個例子中,泄漏壹個單壹字符串不會有太大害處,但這的確是不好的。
另壹種意外全局變量被創建的方式是通過this:
1234567
function foo() {this.variable = "potential accidental global";}// Foo called on its own, this points to the global object (window)// rather than being undefined.foo();
為了阻止這種錯誤發生,在妳的Javascript文件最前面添加'use strict;'。這開啟了解析JavaScript的阻止意外全局的更嚴格的模式。
全局變量的壹個註意事項:
即使我們談了不明的全局變量,仍然存在很多代碼被顯式的全局變量填充的情況。這是通過定義不可收集的情況(除非清零或重新賦值)。特別的,用來臨時存儲和處理大量信息的全局變量會引起關註。如果必須用全局變量來存儲很多數據,在處理完之後,確保對其清零或重新賦值。 壹個在與全局連接上增加內存消耗常見的原因是緩存)。 緩存存儲重復被使用的數據。為此,為了有效,緩存必須有其大小的上限。飆出限制的緩存可能會因為內容不可被回收,導致高內存消耗。
2.被遺忘的計時器或回調
在JavaScript中setInterval的使用相當常見。其他庫提供觀察者和其他工具以回調。這些庫中大多數,在引用的實例變成不可訪問之後,負責讓回調的任何引用也不可訪問。在setInterval的情況下,這樣的代碼很常見:
12345678
var someResource = getData();setInterval(function() {var node = document.getElementById('Node');if(node) {// Do stuff with node and someResource.node.innerHTML = JSON.stringify(someResource));}}, 1000);
這個例子表明了跳動的計時器可能發生什麽:計時器使得節點或數據的引用不再被需要了。代表node的對象將來可能被移除,使得整個塊在間隔中的處理不必要。然而,處理函數,由於間隔仍然是活躍的,不能被回收(間隔需要被停掉才能回收)。如果間隔處理不能被回收,它的依賴也不能被回收。那意味著可能存儲著大量數據的someResource,也不能被回收。
觀察者情況下,壹旦不被需要(或相關的對象快要訪問不到)就創建明確移除他們的函數很重要。在過去,這由於特定瀏覽器(IE6)不能很好的管理循環引用(下面有更多相關信息),曾經尤為重要。現如今,壹旦觀察對象變成不可訪問的,即使收聽者沒有明確的被移除,多數瀏覽器可以並會回收觀察者處理函數。然而,它保持了在對象被處理前明確的移除這些觀察者的好實踐。例如:
12345678910111213
var element = document.getElementById('button');function onClick(event) {element.innerHtml = 'text';}element.addEventListener('click', onClick);// Do stuffelement.removeEventListener('click', onClick);element.parentNode.removeChild(element);// Now when element goes out of scope,// both element and onClick will be collected even in old browsers that don't// handle cycles well.
壹條關於對象觀察者及循環引用的筆記
觀察者和循環引用曾經是JavaScript開發者的禍患。這是由於IE垃圾回收的壹個bug(或者設計決議)出現的情況。IE的老版本不能檢測到DOM節點和JavaScript代碼間的循環引用。 這是壹個通常為觀察到的保留引用(如同上面的例子)的觀察者的典型。 也就是說,每次在IE中對壹個節點添加觀察者的時候,會導致泄漏。這是開發者在節點或空引用之前開始明確的移除處理函數的原因。 現在,現代瀏覽器(包括IE和MS Edge)使用可以剪裁這些循環和正確處理的現代垃圾回收算法。換言之,在使壹個節點不可訪問前,調用removeEventLister不是嚴格意義上必須的。
像Jquery壹樣的框架和庫做了在處置壹個節點前(當為其使用特定的API的時候)移除監聽者的工作。這被在庫內部處理,即使在像老版本IE壹樣有問題的瀏覽器裏面跑,也會確保沒有泄漏產生。
3. 超出DOM引用
有時存儲DOM節點到數據結構中可能有用。假設妳想要迅速的更新壹個表格幾行內容。存儲每個DOM行節點的引用到壹個字典或數組會起作用。當這發生是,兩個對於同個DOM元素的引用被留存:壹個在DOM樹中,另外壹個在字典中。如果在將來的某些點妳決定要移除這些行,需要讓兩個引用都不可用。
123456789101112131415161718192021
var elements = {button: document.getElementById('button'),image: document.getElementById('image'),text: document.getElementById('text')};function doStuff() {image.src = 'http://some.url/image';button.click();console.log(text.innerHTML);// Much more logic}function removeButton() {// The button is a direct child of body.document.body.removeChild(document.getElementById('button'));// At this point, we still have a reference to #button in the global// elements dictionary. In other words, the button element is still in// memory and cannot be collected by the GC.}
對此的額外考慮,必須處理DOM樹內的內部節點或葉子節點。假設妳在JavaScript代碼中保留了壹個對於特定的表格內節點(壹個td標簽)的引用。在將來的某個點決定從DOM中移除這個表格,但是保留對於那個節點的引用。直觀的,會假設GC會回收除那個節點之外的每個節點。在實踐中,這不會發生的:這個單節點是那個表格的子節點,子節點保留對父節點引用。換句話說,來自JavaScript代碼的表格元素的引用會引起在內存裏存整個表格。當保留DOM元素的引用的時候,仔細考慮下。
4.閉包
壹個JavaScript開發的關鍵點是閉包:從父級作用域捕獲變量的匿名函數。很多開發者發現,由於JavaScript runtime的實現細節,有以壹種微妙的方式泄漏的可能,這種特殊的情況:
123456789101112131415
var theThing = null;var replaceThing = function () {var originalThing = theThing;var unused = function () {if (originalThing)console.log("hi");};theThing = {longStr: new Array(1000000).join('*'),someMethod: function () {console.log(someMessage);}};};setInterval(replaceThing, 1000);
這個代碼片段做了壹件事:每次replaceThing被調用的時候,theThing獲取到壹個包括壹個大數組和新閉包(somMethod)的新對象。同時,變量unused保留了壹個有originalThing(theThing從之前的對replaceThing的調用)引用的閉包。已經有點疑惑了,哈?重要的是壹旦壹個作用域被在同個父作用域下的閉包創建,那個作用域是***享的。這種情況下,為閉包somMethod創建的作用域被unused***享了。unused有壹個對originalThing的引用。即使unused從來沒被用過,someMethod可以通過theTing被使用。由於someMethod和unused***享了閉包作用域,即使unused從來沒被用過,它對originalThing的引用迫使它停留在活躍狀態(不能回收)。當這個代碼片段重復運行的時候,可以看到內存使用穩步的增長。GC運行的時候,這並不會減輕。本質上,壹組關聯的閉包被創建(同unused變量在表單中的根節點壹起),這些閉包作用域中每個帶了大數組壹個非直接的引用,導致了大型的泄漏。