基礎進階:超直觀的內存空間詳細圖解

變量對象與堆內存

注:本文轉載自公衆號不知非攻 原文地址javascript

var a = 20;
var b = 'abc';
var c = true;
var d = { m: 20 }
複製代碼

由於JavaScript具備自動垃圾回收機制,因此對於前端開發來講,內存空間並非一個常常被說起的概念,很容易被你們忽視。特別是不少不是計算機專業的朋友在進入到前端以後,會對內存空間的認知比較模糊,甚至有些人乾脆就是一無所知。前端

固然也包括我本身。java

在很長一段時間裏認爲內存空間的概念在JS的學習中並非那麼重要。可當我回過頭來從新整理JS基礎時,發現因爲對它的模糊認知,致使了許多知識理解得並不明白。好比最基本的引用數據類型和引用傳遞究竟是怎麼回事兒?淺複製與深複製有什麼不一樣?閉包究竟是什麼?等等。面試

所以,想要對JS的理解更加深入,就必須對內存空間有一個清晰的認知。算法

在學習內存空間以前,咱們須要對三種數據結構有一個清晰的理解。他們分別是堆(heap),棧(stack)與隊列(queue)編程

1

棧數據結構數組

與C/C++不一樣,JavaScript中並無嚴格意義上區分棧內存與堆內存。所以咱們能夠簡單粗暴的理解爲JavaScript的全部數據都保存在堆內存中。可是在某些場景,咱們仍然須要基於棧數據結構的思惟來實現一些功能,好比JavaScript的執行上下文(關於執行上下文我會在下一篇文章中總結)。執行上下文的執行順序借用了棧數據結構的存取方式(也就是後面咱們會常常提到的函數調用棧)。所以理解棧數據結構的原理與特色十分重要。數據結構

要簡單理解棧的存取方式,咱們能夠經過類比乒乓球盒子來分析。以下圖左側。閉包

乒乓球盒子與棧類比

這種乒乓球的存放方式與棧中存取數據的方式一模一樣。處於盒子中最頂層的乒乓球5,它必定是最後被放進去,但能夠最早被使用。而咱們想要使用底層的乒乓球1,就必須將上面的4個乒乓球取出來,讓乒乓球1處於盒子頂層。這就是棧空間先進後出,後進先出的特色。圖中已經詳細的代表了棧空間的存儲原理。函數

2

堆數據結構

堆數據結構是一種樹狀結構。它的存取數據的方式,則與書架與書很是類似。

書雖然也整齊的存放在書架上,可是咱們只要知道書的名字,就能夠很方便的取出咱們想要的書,而不用像從乒乓球盒子裏取乒乓同樣,非得將上面的全部乒乓球拿出來才能取到中間的某一個乒乓球。比如在JSON格式的數據中,咱們存儲的key-value是能夠無序的,由於順序的不一樣並不影響咱們的使用,咱們只須要關心書的名字。

3

隊列

在JavaScript中,理解隊列數據結構的目的主要是爲了清晰的明白事件循環(Event Loop)的機制究竟是怎麼回事。在後續的章節中我會詳細分析事件循環機制。

隊列是一種先進先出(FIFO)的數據結構。正如排隊過安檢同樣,排在隊伍前面的人必定是最早過檢的人。用如下的圖示能夠清楚的理解隊列的原理。

4

變量對象與基礎數據類型

JavaScript的執行上下文生成以後,會建立一個叫作變量對象的特殊對象(具體會在下一篇文章與執行上下文一塊兒總結),JavaScript的基礎數據類型每每都會保存在變量對象中。

嚴格意義上來講,變量對象也是存放於堆內存中,可是因爲變量對象的特殊職能,咱們在理解時仍然須要將其與堆內存區分開來。

基礎數據類型都是一些簡單的數據段,JavaScript中有5種基礎數據類型,分別是Undefined、Null、Boolean、Number、String。基礎數據類型都是按值訪問,咱們能夠直接操做保存在變量中的實際值。

ES6中新加了一種基礎數據類型Symbol,能夠先不用考慮他

5

引用數據類型與堆內存

與其餘語言不一樣,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.js
var a = 20;
var b = a;
b = 30;

// 這時a的值是多少?
複製代碼
// demo02.js
var 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的執行上下文,做用域鏈,閉包,原型鏈等重要概念。其餘的我會在之後的文章慢慢總結,敬請期待。

6

內存空間管理

由於JavaScript具備自動垃圾收集機制,因此咱們在開發時好像不用關心內存的使用問題,內存的分配與回收都徹底實現了自動管理。可是根據以往的開發經驗,瞭解內存機制有助於本身清晰的認識到本身寫的代碼在執行過程當中發生過什麼,從而寫出性能更加優秀的代碼。

關心內存是一件很是重要的事情。

JavaScript的內存生命週期

  1. 分配你所須要的內存
  2. 使用分配到的內存(讀、寫)
  3. 不須要時將其釋放、歸還

爲了便於理解,咱們使用一個簡單的例子來解釋這個週期。

var a = 20;  // 在內存中給數值變量分配空間
alert(a + 100);  // 使用內存
a = null; // 使用完畢以後,釋放內存空間
複製代碼

第一步和第二步咱們都很好理解,JavaScript在定義變量時就完成了內存分配。第三步釋放內存空間則是咱們須要重點理解的一個點。

JavaScript有自動垃圾收集機制,那麼這個自動垃圾收集機制的原理是什麼呢?其實很簡單,就是找出那些再也不繼續使用的值,而後釋放其佔用的內存。垃圾收集器會每隔固定的時間段就執行一次釋放操做。

在JavaScript中,最經常使用的是經過標記清除的算法來找到哪些對象是再也不繼續使用的,所以a = null其實僅僅只是作了一個釋放引用的操做,讓 a 本來對應的值失去引用,脫離執行環境,這個值會在下一次垃圾收集器執行操做時被找到並釋放。而在適當的時候解除引用,是爲頁面得到更好性能的一個重要方式。

須要注意的是,在局部做用域中,當函數執行完畢,局部變量也就沒有存在的必要了,所以垃圾收集器很容易作出判斷並回收。可是全局變量何時須要釋放內存空間則很難判斷,所以在咱們的開發中,原則上應該避免使用全局變量。

要詳細瞭解垃圾收集機制,建議閱讀《JavaScript高級編程》中的4.3節

相關文章
相關標籤/搜索