在使用JavaScript進行開發的過程中,瞭解JavaScript記憶體機制有助於開發人員能夠清晰的認識到自己寫的程式碼在執行的過程中發生過什麼,也能夠提高專案的程式碼質量。其實關於記憶體的文章也有很多,寫這篇文章也非”重彈老調”,可以說是給自己理解的知識來一個總結,也順便將知識分享給學習JavaScript的小夥伴們。
JavaScript記憶體是怎麼樣的?
JavaScript中的變數的存放有有原始值與引用值之分,原始值代表了原始的資料型別,如Undefined,Null,Number,String,Boolean型別的值;而Object,Function,Array等型別的值便是引用值了。
JavaScript中的記憶體也分為棧記憶體和堆記憶體。一般來說,棧記憶體中存放的是儲存物件的地址,而堆記憶體中存放的是儲存物件的具體內容。對於原始型別的值而言,其地址和具體內容都存在與棧記憶體中;而基於引用型別的值,其地址存在棧記憶體,其具體內容存在堆記憶體中。堆記憶體與棧記憶體是有區別的,棧記憶體執行效率比堆記憶體高,空間相對推記憶體來說較小,反之則是堆記憶體的特點。所以將構造簡單的原始型別值放在棧記憶體中,將構造複雜的引用型別值放在堆中而不影響棧的效率。
1 2 |
var str = "Hello World"; // str:"Hello World"存在棧中 var obj = {value:"Hello World"}; // obj存在棧中,{value:"Hello World"}存在堆中,通過棧中的變數名obj(訪問地址)訪問 |
記憶體中的儲存物件生命週期是怎麼樣的呢?
我們來看看MDN中的介紹:
1.當物件將被需要的時候為其分配記憶體
2.使用已分配的記憶體(讀、寫操作)
3.當物件不在被需要的時候,釋放儲存這個物件的記憶體
第一步和第二步在所有語言中都是一樣的,第三步的操作在JavaScript中不是那麼明顯。
來看看記憶體中發生了什麼?
1 2 3 4 5 6 7 8 9 10 11 |
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"}(兩者在記憶體中引用的是不同物件了)。 |
然後看看這個問題:
1 2 3 4 |
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傳值/址的解說,根據上面對值的原始型別和引用型別的區分,也能夠理解傳的是值還是址。原始型別的值傳的便是值,引用型別的傳的是記憶體中物件的地址。
從程式碼看看區別:
1 2 3 4 5 6 7 8 |
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保持一致。
1 2 3 4 5 6 7 8 9 10 11 |
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也將清理掉這部分記憶體的分配。如:
1 2 |
var obj = {v:1}; // 記憶體中存在{v:1}物件,及obj這個引用地址 obj = {value:2}; // 垃圾回收機制自動清理{v:1},併為新的有用到的{value:2}分配空間 |
某園友的JavaScript垃圾回收機制文章,介紹的也挺詳細。同時這點在《JavaScript高階程式設計》中也有介紹。
記憶體優化
就全域性變數而言,JavaScript不能確定它在後面不能夠被用到,所以它會從宣告之後就一直存在於記憶體中,直至手動釋放或者關閉頁面/瀏覽器,這就導致了某些不必要的記憶體消耗。我們可以進行以下的優化。
使用立即執行函式
1 2 3 |
(function(){ // 你的程式碼 })(); |
或者:
1 2 3 |
(function(window){ // 你的程式碼 })(window); |
如果你的某些變數真的需要一直存在 可以通過上面的方法掛載在window下。同樣,你也可以傳入jQuery進行使用。
手動解除變數的引用:
1 2 |
var obj = {a:1,b:2,c:3}; obj = null; |
在JavaScript中,閉包是最容易產生記憶體問題的,我們可以使用回撥函式代替閉包來訪問內部變數。使用回撥的好處就是(針對訪問的內部變數是原始型別的值,因為在函式傳參的時候傳的是值),在執行完後會自動釋放其中的變數,不會像閉包一樣一直將內部變數存在於記憶體中(但如果是引用型別,那麼這個被引用的物件依舊存在記憶體中)。
1 2 3 4 5 6 7 8 |
function fn_a(){ var value = "Hello World"; return function(){ return value; }; }; var getValue = fn_a(); var v = getValue(); // --> "Hello World" |
在上面的程式碼中,雖然函式已經執行完畢,但是對於函式中變數value的引用還在,所以垃圾回收機制不會將函式中的value清理。
使用回撥:
1 2 3 4 5 6 7 8 |
function fn_a(callback){ var value = "Hello World"; return callback(value); }; function fn_b(arg){ return arg; }; var v = fn_a(fn_b); |
需要注意,使用回撥將會導致非同步。同時宣告,並不是說明這樣做就一定比閉包好,閉包也有其好處,只是需要我們分清何時何地去使用才是恰當的。
這些知識感覺很繞,有疑惑的小夥伴也可留言交流/賜教。本獸也僅剛入門的一隻小前端,不喜勿噴。