前端基礎進階(一):記憶體空間詳細圖解,進階詳細圖解
變數對象與堆記憶體
var a = 20;var b = 'abc';var c = true;var d = { m: 20 }
因為JavaScript具有自動記憶體回收機制,所以對於前端開發來說,記憶體空間並不是一個經常被提及的概念,很容易被大家忽視。特別是很多不是電腦專業的朋友在進入到前端之後,會對記憶體空間的認知比較模糊,甚至有些人乾脆就是一無所知。
當然也包括我自己。在很長一段時間裡認為記憶體空間的概念在JS的學習中並不是那麼重要。可是後我當我回過頭來重新整理JS基礎時,發現由於對它們的模糊認知,導致了很多東西我都理解得並不明白。比如最基本的引用資料類型和引用傳遞到底是怎麼回事兒?比如淺複製與深複製有什麼不同?還有閉包,原型等等。
因此後來我才漸漸明白,想要對JS的理解更加深刻,就必須對記憶體空間有一個清晰的認知。
一、棧與堆
註:棧,也可以叫堆棧
與C/C++不同,JavaScript中並沒有嚴格意義上區分棧記憶體與堆記憶體。因此我們可以粗淺的理解為JavaScript的所有資料都儲存在堆記憶體中。但是在某些情境,我們仍然需要基於堆棧資料結構的思路進行處理,比如JavaScript的執行內容(關於執行內容我會在下一篇文章中總結)。執行內容在邏輯上實現了堆棧。因此理解堆棧資料結構的原理與特點任然十分重要。
要簡單理解棧的存取方式,我們可以通過類比乒乓球盒子來分析。如左側。
乒乓球盒子與棧類比
這種乒乓球的存放方式與棧中存取資料的方式如出一轍。處於盒子中最頂層的乒乓球5,它一定是最後被放進去,但可以最先被使用。而我們想要使用底層的乒乓球1,就必須將上面的4個乒乓球取出來,讓乒乓球1處於盒子頂層。這就是棧空間先進後出,後進先出的特點。圖中已經詳細的表明了棧空間的儲存原理。
堆存取資料的方式,則與書架與書非常相似。
書雖然也整齊的存放在書架上,但是我們只要知道書的名字,我們就可以很方便的取出我們想要的書,而不用像從乒乓球盒子裡取乒乓一樣,非得將上面的所有乒乓球拿出來才能取到中間的某一個乒乓球。好比在JSON格式的資料中,我們儲存的key-value是可以無序的,因為順序的不同並不影響我們的使用,我們只需要關心書的名字。
二、變數對象與基礎資料類型
JavaScript的執行內容產生之後,會建立一個叫做變數對象的特殊對象(具體會在下一篇文章與執行內容一起總結),JavaScript的基礎資料類型往往都會儲存在變數對象中。
嚴格意義上來說,變數對象也是存放於堆記憶體中,但是由於變數對象的特殊職能,我們在理解時仍然需要將其於堆記憶體區分開來。
基礎資料類型都是一些簡單的資料區段,JavaScript中有5中基礎資料類型,分別是Undefined、Null、Boolean、Number、String。基礎資料類型都是按值訪問,因為我們可以直接操作儲存在變數中的實際的值。
三、引用資料類型與堆記憶體
與其他語言不通,JS的引用資料類型,比如數組Array,它們值的大小是不固定的。引用資料類型的值是儲存在堆記憶體中的對象。JavaScript不允許直接存取堆記憶體中的位置,因此我們不能直接操作對象的堆記憶體空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。因此,參考型別的值都是按引用訪問的。這裡的引用,我們可以粗淺地理解為儲存在變數對象中的一個地址,該地址與堆記憶體的實際值相關聯。
為了更好的搞懂變數對象與堆記憶體,我們可以結合以下例子與圖解進行理解。
var a1 = 0; // 變數對象var a2 = 'this is string'; // 變數對象var a3 = null; // 變數對象var b = { m: 20 }; // 變數b存在於變數對象中,{m: 20} 作為對象存在於堆記憶體中var c = [1, 2, 3]; // 變數c存在於變數對象中,[1, 2, 3] 作為對象存在於堆記憶體中
上例圖解
因此當我們要訪問堆記憶體中的引用資料類型時,實際上我們首先是從變數對象中擷取了該對象的地址引用(或者地址指標),然後再從堆記憶體中取得我們需要的資料。
理解了JS的記憶體空間,我們就可以藉助記憶體空間的特性來驗證一下參考型別的一些特點了。
在前端面試中我們常常會遇到這樣一個類似的題目
// demo01.jsvar a = 20;var b = a;b = 30;// 這時a的值是多少?
// demo02.jsvar m = { a: 10, b: 20 }var n = m;n.a = 15;// 這時m.a的值是多少
在變數對象中的資料發生複製行為時,系統會自動為新的變數分配一個新值。var b = a執行之後,a與b雖然值都等於20,但是他們其實已經是相互獨立互不影響的值了。具體。所以我們修改了b的值以後,a的值並不會發生變化。
demo01圖解
在demo02中,我們通過var n = m執行一次複製參考型別的操作。參考型別的複製同樣也會為新的變數自動分配一個新的值儲存在變數對象中,但不同的是,這個新的值,僅僅只是參考型別的一個地址指標。當地址指標相同時,儘管他們相互獨立,但是在變數對象中訪問到的具體對象實際上是同一個。。
因此當我改變n時,m也發生了變化。這就是參考型別的特性。
demo02圖解
通過記憶體的角度來理解,是不是感覺要輕鬆很多。除此之外,我們還可以以此為基礎,一步一步的理解JavaScript的執行內容,範圍鏈,閉包,原型鏈等重要概念。其他的我會在以後的文章慢慢總結,敬請期待。
記憶體空間管理
因為JavaScript具有自動垃圾收集機制,所以我們在開發時好像不用關心記憶體的使用問題,記憶體的分配與回收都完全實現了自動管理。但是根據我自己的開發經驗,瞭解記憶體機制有助於自己清晰的認識到自己寫的代碼在執行過程中發生過什麼,從而寫出效能更加優秀的代碼。因此關心記憶體是一件非常重要的事情。
JavaScript的記憶體生命週期
1. 分配你所需要的記憶體2. 使用分配到的記憶體(讀、寫)3. 不需要時將其釋放、歸還
為了便於理解,我們使用一個簡單的例子來解釋這個周期。
var a = 20; // 在記憶體中給數值變數分配空間alert(a + 100); // 使用記憶體a = null; // 使用完畢之後,釋放記憶體空間
第一步和第二步我們都很好理解,JavaScript在定義變數時就完成了記憶體配置。第三步釋放記憶體空間則是我們需要重點理解的一個點。
JavaScript有自動垃圾收集機制,那麼這個自動垃圾收集機制的原理是什麼呢?其實很簡單,就是找出那些不再繼續使用的值,然後釋放其佔用的記憶體。垃圾收集器會每隔固定的時間段就執行一次釋放操作。
在JavaScript中,最常用的是通過標記清除的演算法來找到哪些對象是不再繼續使用的,因此a = null其實僅僅只是做了一個釋放引用的操作,讓 a 原本對應的值失去引用,脫離執行環境,這個值會在下一次垃圾收集器執行操作時被找到並釋放。而在適當的時候解除引用,是為頁面獲得更好效能的一個重要方式。
- 在局部範圍中,當函數執行完畢,局部變數也就沒有存在的必要了,因此垃圾收集器很容易做出判斷並回收。但是全域變數什麼時候需要自動釋放記憶體空間則很難判斷,因此在我們的開發中,需要盡量避免使用全域變數,以確保效能問題。