1. 程式人生 > >學習 JavaScript (七) 記憶體問題

學習 JavaScript (七) 記憶體問題

記憶體問題是 JavaScript 比較底層的東西,依葫蘆畫瓢學會了怎麼使用變數,但是對於記憶體的概念依然模糊,今天讓我們一起來了解一下記憶體在這門語言是怎麼樣的存在。

記憶體在不同型別的數值面前表現有很大的不同。我們把值賦給一個變數,解析器必須確定這個值是什麼型別,先來了解變數的兩個型別:

  • 基本型別:簡單的資料段:Undefined、Null、Boolean、Number、String,按值訪問,可以直接操作實際的值。

  • 引用型別:儲存在記憶體中的物件:Object、Array。。JavaScript 賦值儲存著物件的某個變數時,操作的是物件的引用;在為物件新增屬性的時候,操作的是實際的物件。

複製基本型別的資料時,計算機會重新分配一個位置給新的變數;但是複製引用型別的資料,計算機只是複製了一個指標,指向原有的物件。所以改變其中一個引用型別資料的屬性時,訪問另一個引用型別資料的屬效能得到一樣的結果,比如:

複製基本型別並改變其中一個變數:

let a = 20;
let b = a;
b = 30;
console.log(a) // 20

基礎型別資料.jpg

複製引用型別並改變其中一個變數的屬性:

let m = {a:10, b:20};
let n = m;
n.a = 15;
console.log(m.a) // 15

引用型別資料.jpg

m,n 都指向一個引用型別的物件,所以改變 n 的屬性會導致 m 的屬性改變。上面表示的是變數之間基本的複製,但是注意:** 在所有函式的引數傳遞中,都是按值傳遞的,不是按照引用傳遞的 **。比如:

function setName(obj){
   obj.name = "Nicholas";
   obj = new Object();
   obj.name = "Greg";
}

let person = new Object();
setName(person);
alert(person.name); // "Nicholas"

JS 記憶體空間分為棧(stack)、堆(heap)、池(一般也會歸類為棧中)。其中「棧」存放基本型別變數,遵循後進先出的原則;「堆」存放引用型別,堆存取資料的方式,則與書架與書非常相似,知道名字就能取出來用;池存放常量。

檢測一個變數是不是基本型別,用 typeof 操作符就可以搞定,但是這個操作符在遇到物件或者 null 時,返回 Object,我們不知道具體的型別。這時候,用 instanceof 來確認是什麼型別的物件。

記憶體洩漏與回收

不再用到的記憶體,沒有及時釋放,就叫做記憶體洩露。

大多數語言提供自動記憶體管理,減輕程式設計師的負擔,這被稱為“垃圾回收機制”(garbage collector)。原理很簡單:找出那些不再繼續使用的變數,然後釋放其佔用的記憶體。

JavaScript 具有自動垃圾收集機制,不用程式設計師操太多心。而不同的瀏覽器可能會採取不同的回收策略,現代瀏覽器最常用的方式是標記清除,其次是引用計數。

  • 標記清除。垃圾收集器會給記憶體中的所有變數都新增標記,然後清除一些還會被使用的標記,即凡是環境中還會用到的變數,被其他變數引用的變數。還有標記的變數就會被垃圾收集器刪除,完成記憶體的清除工作。
  • 引用計數。原理也很簡單,跟蹤每個變數被引用的次數。這裡會產生一個棘手的問題,就是遇到 “迴圈引用” 就沒招了。解決的方法是不再使用的物件,我們把它設定成空物件 Null 。看下面的例子:
function(){
    let a = new Object();
    let b = new Object();
    
    a.oneObject = b;
    b.anotherObject = a;
}

上面的程式碼中,a 和 b 通過各自的屬性實現相互引用,兩者的被引用次數都是 2 。如果採用標記清楚策略,由於函式執行結束,這兩個物件都離開了作用域,都會被清除。但是,採用引用計數策略,a 和 b 都還繼續存在,因為他們的引用次數永遠不會是 0。此時,只有手動斷開引用。

function(){
    let a = new Object();
    let b = new Object();
    
    a.oneObject = b;
    b.anotherObject = a;
    
    // 消除迴圈:
    a.oneObject = null
    b.anotherObject = null;
}