1. 程式人生 > >編寫高效能JavaScript (讀書筆記)

編寫高效能JavaScript (讀書筆記)

轉載自http://kb.cnblogs.com/page/501177/  作者: Addy Osmani

讀書筆記:

作者以V8為例講解:

一.核心構成部分

二.垃圾回收GC:Garbage Collecation

更喜歡叫做資源回收。

Q1:在JavaScript中,是不可能強制進行垃圾回收的。JS中無法強制釋放資源?

  垃圾回收好處是可以大幅簡化程式的記憶體管理程式碼,降低程式設計師負擔,減少因長時間運轉帶來的記憶體洩露問題。

  但使用垃圾回收意味著程式設計師將無法掌控記憶體,ECMAscript沒有暴露任何垃圾回收器的介面,所以我們無法強迫進行垃圾回收和記憶體管理。但是可以通過比較委婉的方式使得GC認為該變數沒用,從而回收。

管理記憶體
使具備垃圾收集機制的語言編寫程式,開發人員一般不必操心記憶體管理的問題。但是,javascript在進行內出你管理及垃圾收集時面臨的問題還是有點與眾不同。其中最重要的一個問題,就是分配給web瀏覽器的可使用記憶體數量通常要比分配給桌面應用程式的少。這樣做的目的出要是處於安全方面的考慮,目的是防止執行javascript的網頁耗盡全部系統記憶體而導致系統崩潰。記憶體限制問題不僅會影響給變數分配記憶體,同時還會影響呼叫棧以及在一個執行緒中能夠同時執行語句數量。
因此,確保佔用最少記憶體可以讓頁面獲得更好的效能,最好通過將其值設定為null來釋放其引用——這個做飯叫做解除引用(dereferencing)。這一做法是用於大多數全域性變數和全域性物件的屬性。區域性變數會在他們執行環境時自動被解除引用,如下面這個例子所示:
function createPerson (name) {
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
};
var gllbalPerson = createPerson("Nicholas");
// 手工解除globalPerson的引用
globalPerson = null;
在這個例子中,變數globalPerson取得了createPerson()函式返回的值。在createPerson()函式內部,我們建立了一個物件並將其賦給了局部變數localPerson,然後又為該物件添加了一個名為name的屬性。最後,當呼叫這個函式時,localPerson以函式的形式返回並賦給全域性變數globalPerson。由於localPerson在createPerson()函式執行完畢後就離開了其執行環境,因此無需我們顯示的去為他解除引用。但是對於全域性變數globalPerson而言,則需要我們在不使用它的時候手工為它解除引用,這也正是上面例子中最後一行程式碼的目的。
不過,解除一個值的引用並不意味著自動回收該值所佔用的記憶體。解除引用的真正作用是讓值脫離執行環境,一邊垃圾收集器下次執行時將其回收。

    Q2:JS中如何進行垃圾回收?

轉載自: http://www.phpchina.com/thread-221214-1-1.html

參考很多大神的講解,1.垃圾回收演算法:

 A.引用計數(IE6/IE7/IE8)

     refernce counting,跟蹤記錄每個值被引用的次數。當宣告一個變數並將引用型別的值賦給該變數時,則這個值的引用次數就是1.如果同一個值又被賦給另一個變數,則該值的引用次數再加1.如果包含對這個值引用的變數又取得另外的值,則這個值的引用次數減1.當這個值的引用次數變成0時,則說明沒有辦法訪問這個值了,因此就可以將其佔用的記憶體空間回收回來。

迴圈引用問題:物件A中包含著對B的引用,B中又包含著對A的引用,使得這2個引數即使離開其作用域,其引用計數不會是0,不會被回收。
function () {
    var objectA = new Object();
     var objectB = new Object();   
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}
IE中有一部分物件並不是原生javascript物件。例如,其中BOM和DOM中的物件就是使用C++以COM (Component Object Model,元件物件模型)物件的形式實現的,
而COM物件的垃圾收集機制採用的就是引用計數策略。因此,即使IE的javascript引擎是使用標記清除策略來實現的,但javascript訪問的COM物件依然是基於引用計數策略的。
換句話說,只要IE中設計COM物件,就會存在迴圈引用的問題。下面這個簡單的例子,展示了使用COM物件導致的迴圈引用問題:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.somObject = myObject;
這裡例子在一個DOM元素(element)與一個原生的javascript物件(myObject)之間建立了迴圈引用。其中,變數myObject有一個名為element的屬性指向element物件;
而變數element也有一個屬性名叫someObject回指myObject。由於存在這個迴圈引用,即使將例子中的DOM從頁面中移除,它也永遠不會被回收。
為了避免類似這樣的迴圈引用問題,最好是不使用他們的時候手工斷開原生javascript物件與DOM元素之間的連線。例如,可以使用下面的程式碼消除前面例子建立的迴圈引用:
myObject.element = null;
element.somObject = null;
將變數設定為null,意味著切斷變數與它此前引用的值之間的連線。但垃圾收集器下次執行時,就會刪除這些值並回收它們佔用的記憶體。
   B.標記清除(現代瀏覽器)
mark-and-sweep,當變數進入環境中時,就將這個變數標記為‘進入環境’,邏輯上講,不能釋放進入環境中的變數所佔用的記憶體,當變數離開環境時,將其標記為‘離開環境’。
可以使用任何方式標記標量,翻轉某個特殊位;使用變數列表記錄;。
GC在執行時會給記憶體中所有的標量加上標記,然後,去掉環境中的變數以及被環境中變數引用的變數的標記。最後銷燬被標記的值並回收其所佔用的空間。感覺像是清理資料夾:先全選所有檔案,再剔除留下要用的檔案,然後delete...
2.回收也會消耗一部分效能,所以瀏覽器在瀏覽器佔用記憶體到一定數值的時候才會執行垃圾回收(也有人說是固定時間間隔)。可以通過強制呼叫方法執行。
效能問題
垃圾收集器都是週期性執行的,而且如果為變數分配的記憶體數量很客觀,那麼回收工作量也是相當大的。在這種情況下,確定垃圾收集的時間間隔是一個非常重要的問題。
說到垃圾收集器多長時間執行一次,不禁讓人聯想到IE因此聲名狼藉的效能問題。IE的垃圾收集器是根據記憶體分配量執行的,具體一點說就是256個變數、4096個物件(或陣列)字面量和陣列元素(slot)或
者64KB的字串。達到上述任何一個臨界值,垃圾收集器就會執行。這種實現的問題在於,如果一個指令碼中包含那麼多 變數,那麼該指令碼很可能會在其生命中起一支保持那麼多的變數。而這樣一來,
垃圾收集器就可能不得不頻繁的執行。結果,由此引發的嚴重效能問題初始IE7重寫了其垃圾收集例程。
隨著IE7的釋出,其javascript引擎的垃圾收集例程改變了工作方式:觸發垃圾收集的變數分配、字面量和(或)陣列元素的臨界值被調整為動態修正。IE7中的各項臨界值在初始化時與IE6相等。
如果例程回收的記憶體分配量低於15%,則變數 、字面量和(或)陣列元素的臨界值就會加倍。如果例程回收了85%的記憶體分配量,則將各種臨界重置會預設值。這一看似簡單的調整,極大地提升了
IE在執行包含大量javascript的頁面時的效能。
事實上,在有的瀏覽器中可以觸發垃圾收集過程,當我們不建議讀者這樣做。在IE中,呼叫window.CollectGarbage()方法會立即指向垃圾收集,在Opera7及更高版本中,呼叫widnow.opera.collect()也會啟動垃圾收集例程。


3.在一個函式中定義的物件,只要沒有引用函式外的其他物件,或者被函式外的其他引用,即使在該函式內相互引用,那麼在該函式執行完後
會被釋放回收。
Q3:記憶體洩露相關 
當一個DOM物件包含一個Js物件的引用(例如一個Event Handler), 而這個Js物件又持有對這個DOM物件的引用時,一個環狀引用就行成了。這本身並不是什麼錯誤或者Bug,因為Js的回收機制能理解這種環狀的引用結構並且在沒有其他物件能關聯到環上的時候回收這個環上的所有物件記憶體。可不幸的是IE瀏覽器中的DOM結構並不受Js解釋機制管理,所以它並不能理解這種失去外界引用的環狀結構,導致環上任何物件都無法被訪問到,可是記憶體依舊佔據著,這也就是所謂的Js記憶體洩露了
由於IE對JScript物件和COM物件使用不同的垃圾收集例程,因此閉包在IE中會導致一些特殊的問題。具體來說,如果閉包的作用域鏈中儲存著一個HTML元素,那麼就意味著該元素無法被銷燬。來看下面的例子:
function assignHandler () {
    var element = document.getElementById("someElement");
    element.onclick = function () {
            alert(element.id);
     };
}; 
以上程式碼建立了一個作為element元素時間處理程式的閉包,而這個閉包則有建立了一個迴圈引用。由於匿名函式儲存了一個對assignHandler()的活動物件的引用,因此就會導致無法減少element的引用數。只要匿名函式存在,element的引用數至少也是1,因此它所佔用的記憶體就永遠不會被回收。不過,這個問題可以通過稍微改寫一下程式碼來解決,如下所示:
function assignHandler () {
    var element = document.getElementById("someElement");
    var id = element.id;   
    element.onclick = function () {
            alert(id);
    }; 
    element = null;
};
在上面程式碼中,通過把element.id的一個副本儲存在一個變數中,並且在閉包中引用該變數消除了迴圈引用。但僅僅做到這一步,還是不能解決記憶體洩漏的問題。必須要記住:閉包會引用包含函式活動的整個活動物件,而其中包含著element。即使閉包不直接引用element,包含函式的活動物件中也仍然會儲存一個引用。因此,有必要把element變數設定為null。這樣就能夠解除對DOM物件的引用,順利地減少其引用數,確保正常回收其佔用的記憶體。
查詢了一些資料,貌似火狐等瀏覽器不會出現這種情況,好吧,萬惡的IE。