1. 程式人生 > >再談JS——JS效能提高之解除引用

再談JS——JS效能提高之解除引用

前言

 其實在V8引擎下,JavaScript的效能已經得到大幅度提高,這裡探討的是在理論層面,具體一點就是JS的垃圾回收機制,可以提高Javascript效能的一種途徑或者方式,也算是一篇讀後總結吧,這裡參考了《JS高程第三版》的第四章有關JS記憶體管理的講解。

正文

 我們先來看看,如何做可以在理論上提高JS的效能,我們來看兩段程式碼:

程式碼段A

function createPerson(name){     
	var localPerson = new Object();     
	localPerson.name = name;     
	return
localPerson; } var globalPerson = createPerson("Nicholas");

程式碼段B

function createPerson(name){     
	var localPerson = new Object();     
	localPerson.name = name;     
	return localPerson; 
} 
 
var globalPerson = createPerson("Nicholas"); 
 
// 手工解除 globalPerson 的引用 
 
globalPerson = null;

 我們看到程式碼片A和程式碼片B的唯一區別就在於,程式碼B在最後將globalPerson變數置為了null,這裡是手工解除globalPerson的引用,以讓變數指向的記憶體空間脫離執行環境,也就是在全域性環境下不再需要globalPerson變數,這樣在JS下一次的自動回收中可以立即回收釋放對應的記憶體,我們下面來探討一下Javascript的記憶體管理,以方便理解這裡的手動解除引用的意思

Javascript的記憶體管理

 在講解Javascript的記憶體自動回收機制前,我們先要對Javascript中一個重要的概念做一個前提性的瞭解,那就是執行環境和作用域

作用域鏈

 不同於Java,C這類語言的塊作用域概念,在Javascript中是沒有塊作用域概念的,我們知道在Java,C中在{}內的變數是區域性變數,在作用域外無法訪問;當然在Javascript中肯定是有全域性和區域性變數之分的,要不然是不可能寫出來JS函式的(會出現變數被汙染的嚴重問題),那Javascript中是如何做到作用域的控制以及全域性與區域性變數的區分的呢?
 答案是通過變數物件的作用域鏈,變數物件是Javascript執行環境中在程式執行時維護的一個不可呼叫的變數,這個變數儲存著執行環境中所有的變數、函式,而每一個變數物件,在程式程式碼執行時會同時生成對應於變數物件的作用域鏈,這個作用域鏈的目的就是保證在執行環境內變數和函式可以按序訪問

執行環境(execution context,為簡單起見,有時也稱為“環境”)是 JavaScript中最為重要的一個概 念。執行環境定義了變數或函式有權訪問的其他資料,決定了它們各自的行為。每個執行環境都有一個 與之關聯的變數物件(variable object),環境中定義的所有變數和函式都儲存在這個物件中。雖然我們 編寫的程式碼無法訪問這個物件,但解析器在處理資料時會在後臺使用它。

 那麼作用域鏈是如何做到區分全域性和區域性變數的呢?我們還以上面的程式碼為例(我們暫時不管解除引用):

function createPerson(name){     
 var localPerson = new Object();     
 localPerson.name = name;     
 return localPerson; 
} 
 
var globalPerson = createPerson("Nicholas"); 

這段程式碼,會生成怎樣的作用域鏈呢?如下圖
在這裡插入圖片描述

我們的程式執行時,如何識別變數就是通過從作用域前端向後不斷回溯搜尋的過程,當在作用域鏈上找到一個符合名稱的變數則搜尋會立即停止,並返回找到的變數。這裡,window是瀏覽器內建的最外圍的執行環境中的物件,這是作用域的最後端,而作用域的前端則是程式執行時需要搜尋作用域鏈的地方。比如,我們瀏覽器解釋JS時執行到了createPerson函式中的localPerson.name = name;,此時需要回溯作用域鏈裡找到localPerson變數的定義,於是在作用域鏈上找到了localPerson,直譯器將停止搜尋並返回這個定義,於是這裡的localPerson就會被解釋為createPerson函式內的區域性變數,我們對程式碼進行一個小的修改:

function createPerson(name){     
 localPerson = new Object();     
 localPerson.name = name;     
 return localPerson; 
}
var globalPerson = createPerson("Nicholas"); 

去掉var後,localPerson會被定義為一個全域性執行環境中的變數,此時搜尋作用域鏈會在全域性作用域鏈中搜索,並返回全域性變數的定義

由於JS這種作用域鏈搜尋的特性,導致了一些會令人迷惑的作用域範圍,比如下面的程式碼:

var a = true;
if(a){
var b = “這是一個全域性執行環境下的變數定義”;
}
console.log(b) //列印 “這是一個全域性執行環境下的變數定義”

這裡和Java,C中不同,JS中變數的作用域範圍不是依據{},而是依據執行環境中的作用域鏈

垃圾自動回收機制

 這一點有點像Java中的垃圾回收,程式設計師不用密切關注記憶體的使用,而更關注與使用語言本身。JS中的垃圾回收機制是基於我們上面說的作用域鏈的,其實實現的方式也很簡單,就是不斷迴圈的去搜索作用域鏈釋放掉已經不使用或者消亡的變數所佔用的記憶體。這裡就產生了一個問題,如何知道執行環境中的變數不再使用或者已經消亡?瀏覽器的實驗一般有兩種方式:標記清除引用計數 。我們這裡簡單介紹一下這兩種方式。

標記清除

這種方式是通過一個標識,當變數或函式被定義時,將標記記為"進入環境",理論上講進入環境的變數,只要在執行流裡就不能被銷燬,因為在執行流的任何地方都又可能使用到變數;同理,當變數已經離開執行流時,將標記記為"離開環境";這裡只是標記,變數所佔用記憶體並沒有立即回收,而是要等下一次迴圈時,才能將標記為"離開環境"的變數的記憶體回收

這裡也啟發我們,不要濫用全域性變數,因為這對JS回收記憶體不友好

引用計數

引用計數相對巧妙些,這個是對引用型別的值而言的,我們知道在JS中引用型別的值的複製是引用複製,複製的是地址,比如:

var objectA = new Object();
var objectB = objectA;
objectA.name = "angellover";
console.log(objectB.name);//輸出 ”angellover“

這裡的objectAobjectB指向的是同一個堆記憶體,所以當改變objectAobjectB也會被改變。
而這裡的引用計數,就是說當引用型別的值被指向某個變數時,引用計數加一;同理,當引用型別的值與某個變數不再關聯時,則引用計數減一;這樣,當引用計數再次達到0時,就會被認為此記憶體不再被需要,在下次垃圾回收迴圈中會被釋放

現在瀏覽器中的實現方法主要是標記清除,因為引用計數的方法不能解決迴圈引用的問題,迴圈引用這裡不再講解,只要知道有這樣的問題就好

為什麼解除引用可以提高JS效能

 通過上面的講解,其實你應該已經知道了答案,globalPerson = null;是為了斷開變數與記憶體之間的聯絡,這樣當下一次垃圾回收迴圈中就會釋放掉;這裡globalPerson是全域性執行環境中的變數,如果不進行解除引用,則在整個執行流中都不會釋放掉對應的記憶體,如果JS檔案不大的話還好,當JS檔案很大的時候效能的問題就會體現出來

對於全域性變數,當不再使用時,使用null賦值是一個良好的習慣

另外,在瀏覽器中記憶體是比較珍貴的資源,對於作業系統而言,分配給桌面應用程式的記憶體一般比瀏覽器要高,這是因為害怕瀏覽器隨著視窗增多,過大佔用記憶體導致一些其他問題,所以考慮如何提高JS效能還是一件值得探討的問題