JavaScript 中對內存的一些了解


在使用JavaScript進行開發的過程中,了解JavaScript內存機制有助於開發人員能夠清晰的認識到自己寫的代碼在執行的過程中發生過什么,也能夠提高項目的代碼質量。其實關於內存的文章也有很多,寫這篇文章也非"重彈老調",可以說是給自己理解的知識來一個總結,也順便將知識分享給學習JavaScript的小伙伴們。

JavaScript內存是怎么樣的?

JavaScript中的變量的存放有有原始值與引用值之分,原始值代表了原始的數據類型,如Undefined,Null,Number,String,Boolean類型的值;而Object,Function,Array等類型的值便是引用值了。

JavaScript中的內存也分為棧內存和堆內存。一般來說,棧內存中存放的是存儲對象的地址,而堆內存中存放的是存儲對象的具體內容。對於原始類型的值而言,其地址和具體內容都存在與棧內存中;而基於引用類型的值,其地址存在棧內存,其具體內容存在堆內存中。堆內存與棧內存是有區別的,棧內存運行效率比堆內存高,空間相對推內存來說較小,反之則是堆內存的特點。所以將構造簡單的原始類型值放在棧內存中,將構造復雜的引用類型值放在堆中而不影響棧的效率。

var str = "Hello World"; // str:"Hello World"存在棧中
var obj = {value:"Hello World"}; // obj存在棧中,{value:"Hello World"}存在堆中,通過棧中的變量名obj(訪問地址)訪問

內存中的存儲對象生命周期是怎么樣的呢?

我們來看看MDN中的介紹:

1.當對象將被需要的時候為其分配內存

2.使用已分配的內存(讀、寫操作)

3.當對象不在被需要的時候,釋放存儲這個對象的內存

第一步和第二步在所有語言中都是一樣的,第三步的操作在JavaScript中不是那么明顯。

來看看內存中發生了什么?

var str_a = "a"; // 為str_a分配棧內存:str_a:"a"
var str_b = str_a; // 原始類型直接訪問值,so,為str_b新分配棧內存:str_b:"a"

str_b = "b"; // 棧內存中:str_b:"b"。str_b的值為"b",而str_a的值仍然是"a"
// 分隔 str 和 obj -----------------------------------------------------------//
var obj_a = {v:"a"}; // 為obj_a分配棧內存訪問地址:obj_a,堆內存中存對象值:{v:"a"};
var obj_b = obj_a; // 為obj_b分配棧內存訪問地址:obj_b,引用了堆內存的值{v:"a"}

obj_b.v = "b"; // 通過obj_b訪問(修改)堆內存的變量,這時候堆內存中對象值為:{v:"b"},由於obj_a和obj_b引用的是堆內存中同一個對象值,所以這時候打印都是{v:"b"}

obj_b = {v:"c"}; // 因為改的是整個對象,這里會在堆內存中創建一個新的對象值:{v:"c"},而現在的obj_b引用的是這個對象,所以這里打印的obj_a依舊是{v:"b"},而obj_b是{v:"c"}(兩者在內存中引用的是不同對象了)。

然后看看這個問題:

var a = {n:1}; 
var b = a; 
a.x = a = {n:2}; 
// a:{n:2} a.x=undefined b:{n:1,x:{n:2}} b.x:{n:2}

具體的解釋可以看看某位園友的詳細解釋,對理解基礎知識點還是很有幫助的哦。

從內存角度看函數傳值的變化

網上不少文章是關於JavaScript傳值/址的解說,根據上面對值的原始類型和引用類型的區分,也能夠理解傳的是值還是址。原始類型的值傳的便是值,引用類型的傳的是內存中對象的地址。

從代碼看看區別:

var str_a = "Hello World";
function fn_a(arg){
    console.log(arg); // #1 --> Hello World
    arg = "Hai";
    console.log(str_a,arg); // #2 --> Hello World , Hai
};
fn_a(str_a);
// #3 這時候str_a:"Hello World"

從上面#1處可以看出,傳入函數fn_a的是str_a的值(這時候和之前案例str_a/str_b的情況一樣),並且內存中分配了新的空間來保存函數參數和其值(函數運行后自動釋放這部分內存,后面或說回收機制),所以在#2處打印的是2個不同的字符串。也正是因為傳值時候對str_a值進行了值的復制,而這又是原始類型的值,所以在#3處的str_a與早先聲明的str_a保持一致。

var obj_a = {value:1};
function fn_a(arg){
    arg={value:2};
};
fn_a(obj_a);
// 這時候obj_a還是{value:1}
function fn_b(arg){
    arg.value=3;
};
fn_b(obj_a);
// 這時候obj_a是{value:3}

上面這個問題也可以從內存角度去理解,兩個函數都是傳址,而這個址引用了obj_a在內存中對應的對象,所以兩個函數中的arg起初都是引用和obj_a同一個內存中的對象值,但是在fn_a中重新為arg賦值新的對象(和之前例子中的obj_a/obj_b情況一樣),而fn_b中訪問的依舊是和obj_a同一個內存對象,所有fn_b修改是成功的。

垃圾回收機制(簡單帶過)

JavaScript具有自動進行垃圾回收的機制,這便造成了開發人員極大的方便,至少不用太考慮內存釋放的問題(有部分還是要考慮的)。

1.函數的變量只在函數執行過程中存在。在函數執行過程中,函數內部的變量將會在內存中被分配一定的空間,當函數執行完畢后,自動將這些變量從內存中釋放,以留出空間作其他用處。

2.當內存中某個變量不再被引用,JavaScript也將清理掉這部分內存的分配。如:

var obj = {v:1}; // 內存中存在{v:1}對象,及obj這個引用地址
obj = {value:2}; // 垃圾回收機制自動清理{v:1},並為新的有用到的{value:2}分配空間 

某園友的JavaScript垃圾回收機制文章,介紹的也挺詳細。同時這點在《JavaScript高級程序設計》中也有介紹。

內存優化

就全局變量而言,JavaScript不能確定它在后面不能夠被用到,所以它會從聲明之后就一直存在於內存中,直至手動釋放或者關閉頁面/瀏覽器,這就導致了某些不必要的內存消耗。我們可以進行以下的優化。

使用立即執行函數

(function(){
    // 你的代碼
})();

或者:

(function(window){
    // 你的代碼
})(window);

如果你的某些變量真的需要一直存在 可以通過上面的方法掛載在window下。同樣,你也可以傳入jQuery進行使用。

手動解除變量的引用

var obj = {a:1,b:2,c:3};
obj = null;

在JavaScript中,閉包是最容易產生內存問題的,我們可以使用回調函數代替閉包來訪問內部變量。使用回調的好處就是(針對訪問的內部變量是原始類型的值,因為在函數傳參的時候傳的是值),在執行完后會自動釋放其中的變量,不會像閉包一樣一直將內部變量存在於內存中(但如果是引用類型,那么這個被引用的對象依舊存在內存中)。

function fn_a(){
    var value = "Hello World";
    return function(){
            return value;
        };
};
var getValue = fn_a();
var v = getValue(); // --> "Hello World"

在上面的代碼中,雖然函數已經執行完畢,但是對於函數中變量value的引用還在,所以垃圾回收機制不會將函數中的value清理。
使用回調:

function fn_a(callback){
    var value = "Hello World";
    return callback(value);
};
function fn_b(arg){
    return arg;
};
var v = fn_a(fn_b);

同時聲明,並不是說明這樣做就一定比閉包好,閉包也有其好處,只是需要我們分清何時何地去使用才是恰當的。

歡迎交流,共同進步~ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM