內存管理與垃圾回收

首先咱們須要理解,內存是什麼。簡單來說,內存存儲了計算機運行過程的須要的所有數據,也就是計算機正在使用的所有數據。咱們須要合理的使用內存,防止內存被大量無用數據佔用,同時也要防止訪問和修改與當前程序無關的內存區域。內存主要包括如下幾個部分: 內核數據區域,棧區,共享庫映像,堆區,可讀寫區域,只讀區域。學習javascript,咱們不須要理解內存和cache,內存和I/O之間具體工做原理,但咱們須要瞭解掌握如何合理的使用內存,合理的分配釋放內存。javascript

javascript的內存管理

Javascript 是那些被稱做垃圾回收語言當中的一員。垃圾回收語言經過週期性地檢查那些以前被分配出去的內存是否能夠從應用的其餘部分訪問來幫助開發者管理內存。換句話說,當計算機發現有的內存已經不能被訪問到了,就會把它們標記爲垃圾。開發者只須要知道一塊已分配的內存是否會在未來被使用,而不可訪問的內存能夠經過算法肯定並標記以便返還給操做系統。java

引用傳遞和值傳遞

js中的變量除了6個基本類型之外,其他的都是對象。也就說基本類型在賦值是傳遞的是值,也就是原來數據的一份拷貝。基本類型包括number、string、boolean、symbol、null、undefined.
用2個例子來理解一下:算法

值傳遞

var a = 10;  //基本類型
var b = a;   //a把10拷貝一份,把這個拷貝給b
a = 20;  //修改了a,不影響a的拷貝
console.log(a);  //20
console.log(b);  //10

引用傳遞

var a = {num: 20};  //不是基本類型
var b = a;   //這裏沒有任何拷貝工做,b指向和a徹底一致的同一塊內存
b.num = 15;  //因爲b和a指向同一塊內存,因此b.num修改了等同於a.num修改了
console.log(a.num); //15
console.log(b.num); //15

//進一步理解
b = {age: 10};  //等號右邊定義了一個新的對象,產生的新的內存分配,此時b指向了這塊新的內存,a仍是指向原來那塊內存
console.log(a);  //{num: 15}
console.log(b);  //{age: 10}

值得強調的是:在函數參數傳遞的時候和返回值時同樣遵照這個傳遞規則,這是構成閉包的基礎條件數組

簡單的函數傳遞參數

//一個反例
var num1 = 10;
var num2 = 20;
function swap(a, b){
    var temp = a;
    a = b;
    b = temp;
}
swap(num1, num2);
console.log(num1);  //10
console.log(num2);  //20

以上代碼不能如願的把2個傳入變量的值交換,由於基本類型在參數傳遞時也是值傳遞,及a,b是num1,num2的拷貝,不是num1和num2自己。固然實現交換的方法不少,在不引入第三個變量狀況下,不用單獨寫一個函數。瀏覽器

//實現交換a,b
//方法1:
var temp = a;
a = b;
b =temp;

//方法2:
a = a + b;
b = a - b;
a = a - b;

//方法3:
a = [b, b = a][0];

//方法4(僅適用於整數交換):
a = a ^ b; // ^表示異或運算
b = a ^ b; 
a = a ^ b; 

//方法5:
[a, b] = [b, a]; //解構賦值

閉包的原理

var inc = function(){
    var x = 0;
    return function(){  //返回一個非基本類型
        console.log(x++);
    };
};
inc1 = inc();  //inc1是閉包內匿名函數的引用,因爲該引用存在,匿名函數引用計數不爲0,因此inc做用域對應的內存不能釋放,閉包造成
inc1();  //0
inc1();  //1

淺拷貝與深拷貝

當對象的屬性是對象的時候,簡單地賦值致使改屬性傳遞的是另外一個對象屬性的引用,這樣的拷貝是淺拷貝,存在安全風險。咱們應該遞歸的拷貝對象屬性的每一個對象,造成深拷貝。方法以下:安全

//淺拷貝與深拷貝
var o = {
    name: "Lily",
    age: 10,
    addr:{
        city: "Shenzheng",
        province: "Guangdong"
    },
    schools: ["primaryS", "middleS", "heightS"]
};
var newOne = copy(o);
console.log(o);   //Object {name: "Lily", age: 10, addr: Object}
console.log(newOne);   //Object {name: "Lily", age: 10, addr: Object}

newOne.name = "Bob";
console.log(newOne.name);  //"Bob"
console.log(o.name);   //"Lily"

newOne.addr.city = "Foshan";
console.log(newOne.addr.city);   //"Foshan"
console.log(o.addr.city);   //"Foshan"

function copy(obj){
    var obj = obj || {};
    var newObj = {};
    for(prop in obj){
        if(!obj.hasOwnProperty(prop)) continue;
        newObj[prop] = obj[prop];  //當obj[prop]不是基本類型的時候,這裏傳的時引用
    }
    return newObj;
}

var newOne = deepCopy(o);
console.log(o);   //Object {name: "Lily", age: 10, addr: Object}
console.log(newOne);   //Object {name: "Lily", age: 10, addr: Object}

newOne.name = "Bob";
console.log(newOne.name);  //"Bob"
console.log(o.name);   //"Lily"

newOne.addr.city = "Foshan";
console.log(newOne.addr.city);   //"Foshan"
console.log(o.addr.city);   //"Shenzheng"

newOne.schools[0] = "primatrySchool";
console.log(newOne.schools[0]);   //"primatrySchool"
console.log(o.schools[0]);   //"primatryS"

function deepCopy(obj){
    var obj = obj || {};
    var newObj = {};
    deeply(obj, newObj);
    
    function deeply(oldOne, newOne){
        for(var prop in oldOne){
            if(!oldOne.hasOwnProperty(prop)) continue;
            if(typeof oldOne[prop] === "object" && oldOne[prop] !== null){
                newOne[prop] = oldOne[prop].constructor === Array ? [] : {};
                deeply(oldOne[prop], newOne[prop]);
            }
            else
                newOne[prop] = oldOne[prop];
        }
    }
    return newObj;
}

變量定義和內存釋放

不一樣的變量定義方式,會致使變量不能被刪除,內存沒法釋放。閉包

// 定義三個全局變量
var global_var = 1;
global_novar = 2; // 反面教材
(function () {
    global_fromfunc = 3; // 反面教材
}());

// 試圖刪除
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true

// 測試該刪除
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"

很明顯,經過var定義的變量沒法被釋放。app

垃圾回收與內存泄漏

垃圾回收(Garbage Collection),簡稱GC。簡單來說,GC就是把內存中不須要的數據釋放了,這樣這部份內存就能夠存放其餘東西了。在javascript中,若是一個對象再也不被引用,那麼這個對象就會被GC回收。具體回收策略包括如下3種:dom

標記回收

當從window節點遍歷DOM樹不能遍歷到某個對象,那麼這個對象就會被標記爲沒用的對象。因爲回收機制是週期性執行的,這樣,當下一個回收週期到來時,這個對象對應的內存就會被釋放。函數

引用計數

當系統中定義了一個對象後,對於這一塊內存,javascript會記錄有多少個引用指向個部份內存,若是這個數爲零,則這部份內存會在下一個回收週期被釋放。

手動釋放

就比如上一個例子中,利用delete關鍵字刪除變量或屬性,達到釋放內存的目的。分一下幾種狀況:

//釋放一個對象
obj = null;

//釋放是個對象屬性
delete obj.propertyName;
delete globalVariable;  //沒有用var聲明的變量是window的屬性,用delete釋放。

//釋放數組
array.length = 0;

//釋放數組元素
array.splice(2,2);  //刪除並釋放第三個元素起的2個元素

不過須要注意的是, 這幾個GC策略是同時做用的:

var o1 = {};  //開闢一塊內存放置對象,並用o1指向它
var o2 = o1;  //o2指向與o1同一個內存區域
console.log(o1);  //{}
console.log(o2);  //{}
o2 = null;  //標記o2爲沒用的對象
console.log(o2);  //null
console.log(o1);  //{}  因爲還有o1指向這個內存區域,引用計數不爲零,因此內存並無被釋放
o1 = null;  //引用計數爲0, 內存釋放

若是你訪問了已經被回收了的內存,會發生不可預計的嚴重後果。好比一段內存被釋放了,可能裏面的值就不是原來的值了,你還要拿來用那不是本身找錯誤嗎?更嚴重的就是你修改了其餘程序的數據!!!咱們將這樣的變量叫作野指針(wild pointer)。爲了不這樣的也只能出現,也爲了節省計算機資源,咱們須要防止內存泄露(memory leak)。

內存泄漏也稱做存儲滲漏,用動態存儲分配函數動態開闢的空間,在使用完畢後未釋放,結果致使一直佔據該內存單元,直到程序結束。簡單來講就是該內存空間使用完畢以後未回收。

內存泄露是每一個開發者最終都不得不面對的問題。即使使用自動內存管理的語言,你仍是會碰到一些內存泄漏的狀況。內存泄露會致使一系列問題,好比:運行緩慢,崩潰,高延遲,甚至一些與其餘應用相關的問題。

可能致使內存泄漏的操做

  • 清除因此子元素用innerHTML=""替代removeChild(),由於在sIEve中監測的結果是用removeChild沒法有效地釋放dom節點。
//反例
var parent = document.getElementById("parent");
var first = parent.firstChild();
while(first){  //循環屢次觸發reflow,效率過低
    parent.removeChild(first);  //在舊的瀏覽器上會致使內存泄漏
    first = parent.firstChild();
}

//正解
document.getElementById("parent").innerHTML = 「」;
  • 綁定事件的元素是不能在remove時被清理的,應該在remove以前取消事件綁定。不過更好的辦法是用事件委託的方式綁定事件。
var ele = document.getElementById("eleID");
ele.onclick = function fun(){
    //Do stuff here
}
//...
ele.onclick = null;  //刪除元素前取消全部事件,jQuery中也是在刪除節點前利用removeEventListen去除了對應事件
ele.parentNode.removeChild(ele);
  • 意外的全局變量,會使得實際函數結束就應該釋放的內存保留下來,形成資源浪費,包括如下兩種狀況:

在嚴格模式下編寫代碼能夠避免這個問題

//狀況一: 函數中沒有用var聲明的變量
function fun1(){
    name = "Mary";   //全局變量
}
fun1();

//狀況二: 構造函數沒用new關鍵字調用
function Person(name){
    this.name = name;
}
Person("Mary");   //函數內定義全局變量
  • 定時器中的變量定義,會在每次執行函數的時候重複定義變量,產生嚴重的內存泄漏。
//反例
setInterval(function(){
    var ele = document.getElementById("eleID");  //改代碼毎100毫秒會重複定義該引用
    //Do stuff
}, 100);

//正解
setInterval(function(){
    var ele = document.getElementById("eleID");  //改代碼毎100毫秒會重複定義該引用
    //Do stuff
    ele = null;
}, 100);
  • 若是閉包的做用域鏈中保存着一個DOM對象或者ActiveX對象,那麼就意味着該元素將沒法被銷燬:
//反例
//不妨認爲這裏的上下文是window
function init(){
    var el = document.getElementById('MyElement'); //這是一個DOM元素的引用,非基本類型
    el.onclick = function(){  //el.onclick是function匿名函數的引用
         alert(el.innerHTML);  //funciton中訪問了這個做用域之外的DOM元素引用el,致使el不能被釋放
    }
 }
 init();

 //正解
 function init(){
    var el = document.getElementById('MyElement'); //這是一個DOM元素的引用,是非基本類型
    var text = el.innerHTML; //字符串,是基本類型,解決alert(el.innerHTML)不能正常工做問題
    el.onclick = function(){  //el.onclick是function匿名函數的引用
         alert(text);  //var聲明的text是基本類型,沒必要釋放
    }
    el = null;  //手動釋放,但會致使alert(el.innerHTML)不能正常工做
 }
 init();

 //若是函數結尾要return el,用如下方法釋放el
 //正解
function init(){
    var el = document.getElementById('MyElement'); //這是一個DOM元素的引用,是非基本類型
    var text = el.innerHTML; //字符串,是基本類型,解決alert(el.innerHTML)不能正常工做問題
    el.onclick = function(){  //el.onclick是function匿名函數的引用
         alert(text);  //var聲明的text是基本類型,沒必要釋放
    }
    try{
        return el;
    } finally {
        el = null;  //手動釋放,但會致使alert(el.innerHTML)不能正常工做
    }
 }
 init();
  • 經過createElement,createTextNode等方法建立的元素會在寫入DOM後被釋放
function create() {
    var parent = document.getElementById('parent');
    for (var i = 0; i < 5000; i++) {
        var el = document.createElement('div');
        el.innerHTML = "test";
        gc.appendChild(el);  //這裏釋放了內存
    }
}
  • 循環引用致使引用計數永遠不爲0,內存沒法釋放:
//構成一個循環引用
var o1 = {name: "o1"};
var o2 = {name: "o2"};
o1.pro = o2;
o2.pro = o1;

//這種狀況須要手動清理內存,在不須要的時候把對象置爲null或刪除pro屬性
相關文章
相關標籤/搜索