1. 程式人生 > >JavaScript js呼叫堆疊(二)

JavaScript js呼叫堆疊(二)

本文主要介紹JavaScript的記憶體空間

var a = 20;
var b = 'abc';
var c = true;
var d = { m: 20 }

首先需要對棧(stack),堆(heap),與佇列(queue)有一定的瞭解:

  • 棧(stack)

  

這種乒乓球的存放方式與棧中存取資料的方式如出一轍。處於盒子中最頂層的乒乓球5,它一定是最後被放進去,但可以最先被使用。而我們想要使用底層的乒乓球1,就必須將上面的4個乒乓球取出來,讓乒乓球1處於盒子頂層。這就是棧空間 先進後出,後進先出的特點。圖中已經詳細的表明了棧空間的儲存原理。
  • 堆資料結構

堆資料結構是一種樹狀結構。它的存取資料的方式,則與書架與書非常相似。

書雖然也整齊的存放在書架上,但是我們只要知道書的名字,我們就可以很方便的取出我們想要的書,而不用像從乒乓球盒子裡取乒乓一樣,非得將上面的所有乒乓球拿出來才能取到中間的某一個乒乓球。好比在JSON格式的資料中,我們儲存的key-value是可以無序的,因為順序的不同並不影響我們的使用,我們只需要關心書的名字

  • 佇列

在JavaScript中,理解佇列資料結構的目的主要是為了清晰的明白事件迴圈(Event Loop)的機制到底是怎麼回事。

佇列是一種先進先出(FIFO)的資料結構。正如排隊過安檢一樣,排在隊伍前面的人一定是最先過檢的人。用以下的圖示可以清楚的理解佇列的原理。

 

變數物件與基礎資料型別:

JavaScript的執行上下文生成之後,會建立一個叫做變數物件的特殊物件(上一篇已總結),JavaScript的基礎資料型別往往都會儲存在變數物件中,即儲存在記憶體中,因為這些型別在記憶體中分別佔有固定大小的空間,通過按值來訪問。

JavaScript中有5種基礎資料型別,分別是 Undefined、Null、Boolean、Number、String、Symbol。基礎資料型別都是按值訪問,因為我們可以直接操作儲存在變數中的實際的值。

引用資料型別與堆記憶體:
JS的引用資料型別,比如陣列Array,Object,它們值的大小是不固定的。引用資料型別的值是儲存在堆記憶體中的物件。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] 作為物件存在於堆記憶體中

因此當我們要訪問堆記憶體中的引用資料型別時,實際上我們首先是從變數物件中獲取了該物件的地址引用(或者地址指標),然後再從堆記憶體中取得我們需要的資料。

在計算機的資料結構中,棧比堆的運算速度快,Object是一個複雜的結構且可以擴充套件:陣列可擴充,物件可新增屬性,都可以增刪改查。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查詢到堆中的實際物件再進行操作。所以查詢引用型別值的時候先去查詢再去查詢。對於這種,我們把它叫做按引用訪問。

 幾個問題

 

var a = 20;
var b = a;
b = 30;

// 這時a的值是多少?

 

var m = { a: 10, b: 20 }
var n = m;
n.a = 15;

// 這時m.a的值是多少

 

var m = { a: 10, b: 20 }
var n = m;
m= null;

// 這時n的值是多少

這三個問題的答案分別是:20, 15,  { a: 10, b: 20 }

  • 問題一:a、b都是基本資料型別,它們的值是儲存在棧中的,a、b分別有各自獨立的棧空間,所以修改了b的值以後,a的值並不會發生變化。
  • 問題二:m、n都是引用型別,棧記憶體中存放地址指向堆記憶體中的物件,引用型別的複製會為新的變數自動分配一個新的值儲存在變數物件中,但只是引用型別的一個地址指標而已,實際指向的是同一個物件,所以修改n.a的值後,相應的m.a也就發生了改變。
  • 問題三:首先要說明的是null是基本型別,m = null之後只是把m儲存在棧記憶體中地址改變成了基本型別null,並不會影響堆記憶體中的物件,所以n的值不受影響。

JavaScript的記憶體生命週期

  1. 分配你所需要的記憶體
  2. 使用分配到的記憶體(讀,寫)
  3. 不需要時將其釋放,歸還

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

 

var a = 20;  // 在記憶體中給數值變數分配空間
alert(a + 100);  // 使用記憶體
a = null; // 使用完畢之後,釋放記憶體空間

 

第一步和第二步我們都很好理解,JavaScript在定義變數時就完成了記憶體分配。第三步釋放記憶體空間則是我們需要重點理解的一個點。

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

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

 

在區域性作用域中,當函式執行完畢,區域性變數也就沒有存在的必要了,因此垃圾收集器很容易做出判斷並回收。但是全域性變數什麼時候需要自動釋放記憶體空間則很難判斷,因此在我們的開發中,需要儘量避免使用全域性變數。

 

附: 某些情況下,呼叫堆疊中函式呼叫的數量超出了呼叫堆疊的實際大小,瀏覽器會丟擲一個錯誤終止執行。 對於上面的遞迴就會無限制的執行下去,直到超出呼叫堆疊的實際大小,這個是瀏覽器定義的。

1、標記清除法:

JavaScript最常用的垃圾收集方式。當變數進入環境時,這個變數標記為“進入環境”;而當變數離開環境時,則將其標記為“離開環境”。可以使用一個“進入環境”的變數列表及一個“離開環境”的變數列表來跟蹤變數的變化,也可以翻轉某個特殊的位來記錄一個變數何時進入環境及離開環境。

2、引用計數法:

不太常見的垃圾收集策略。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變數並將一個引用型別值賦給該變數時,則該值的引用次數就是1;如果同一個值又被賦給另一個變數,則該值的引用次數加1;如果包含對該值引用的變數又取得了另外一個值,則該值的引用次數減1。當該值的引用次數變為0時,則可以回收其佔用的記憶體空間。當垃圾回收器下一次執行時,就會釋放那些引用次數為0的值所佔用的記憶體。


參考:
https://www.jianshu.com/p/996671d4dcc4