js-棧、堆、佇列、記憶體空間
棧(stack) 、堆(heap)、 佇列(queue)是js的三種資料結構。
棧(stack)
棧的特點是"LIFO,即後進先出(Last in, first out)"。資料儲存時只能從頂部逐個存入,取出時也需從頂部逐個取出。
舉個栗子:乒乓球盒子/搭建積木
JavaScript中Array陣列模擬棧:
var arr = [1, 2, 3, 4, 5];
arr.push(6); // 存入資料 arr -> [1, 2, 3, 4, 5, 6]
arr.pop(); // 取出資料 arr -> [1, 2, 3, 4, 5]
堆(heap)
堆的特點是"無序"的key-value
舉個栗子:書架存書
我們想要在書架上找到想要的書,最直接的方式就是通過查詢書名,書名就是我們的key。拿著這把key,就可以輕鬆檢索到對應的書籍。
"堆的存取方式跟順序沒有關係,不侷限出入口"。
佇列 (queue)
佇列的特點是是"FIFO,即先進先出(First in, first out)" 。
資料存取時"從隊尾插入,從隊頭取出"。
"與棧的區別:棧的存入取出都在頂部一個出入口,而佇列分兩個,一個出口,一個入口"。
舉個栗子:排隊取餐
JavaScript中Array陣列模擬佇列:
var arr = [1, 2, 3, 4, 5]; // 隊尾in arr.push(6); // 存入 arr -> [1, 2, 3, 4, 5, 6] // 隊頭out arr.shift(); // 取出 arr -> [2, 3, 4, 5, 6]
棧、堆、佇列在JavaScript中的應用
1. 程式碼執行方式(棧應用/函式呼叫棧)
JavaScript中函式的執行過程,其實就是一個入棧出棧的過程:
- 當指令碼要呼叫一個函式時,JS解析器把該函式推入棧中(push)並執行
- 當函式執行結束後,JS解析器將它從堆疊中推出(pop)
2. 記憶體儲存(棧、堆)
JavaScript中變數型別有兩種:
- 基礎型別(
Undefined, Null, Boolean, Number, String, Symbol
)一共6種 - 引用型別(
Object
)
JS中的基礎資料型別,這些值都有固定的大小,往往都儲存在棧記憶體中(閉包除外),由系統自動分配儲存空間。我們可以直接操作儲存在棧記憶體空間的值,因此基礎資料型別都是按值訪問 資料在棧記憶體中的儲存與使用方式類似於資料結構中的堆疊資料結構,遵循後進先出的原則。
JS的引用資料型別,比如陣列Array,它們值的大小是不固定的。引用資料型別的值是儲存在堆記憶體中的物件。JS不允許直接訪問堆記憶體中的位置,因此我們不能直接操作物件的堆記憶體空間。在操作物件時,實際上是在操作物件的引用而不是實際的物件。因此,引用型別的值都是按引用訪問的。這裡的引用,我們可以粗淺地理解為儲存在棧記憶體中的一個地址,該地址與堆記憶體的實際值相關聯。
3. 事件輪詢(佇列)
JavaScript中事件輪詢(Event Loop)的執行機制,就是採用佇列的存取方式,因事件輪詢(Event Loop)也是JS基礎中的一個比較難理解的知識點,後續另開一篇章再作詳細探究。
深淺拷貝
將一個變數的值賦值給另一個變數,相當於在棧記憶體中建立了一個新的記憶體空間,然後從棧中複製值,儲存到這個新空間中。對於基本型別,棧中儲存的就是它自身的值,所以新記憶體空間儲存的也是一個值。直接改變新變數的值,不會影響到舊變數的值,因為他們值儲存的記憶體空間不同。
// 基本型別複製變數
var a = 10;
var b = a;
b = 20;
a // 10
b // 20
而對於引用型別來說,同樣是複製棧中儲存的值。但是棧儲存的只是其引用地址,其具體的值儲存在堆中。變數複製僅複製棧中儲存的值,不會複製堆中儲存的值,所以新變數在棧中的值是一個地址指標。
// 引用型別複製變數
var a = { age: 27 };
var b = a;
b.age = 29;
a.age == b.age; // 29
可見,變數複製賦值,都屬於棧儲存拷貝,因此深淺拷貝可以這樣區分:
- "淺拷貝:棧儲存拷貝"
- "深拷貝:棧堆儲存拷貝"
深拷貝會同時開闢新的棧記憶體,堆記憶體空間。
// 利用JSON物件方法實現深拷貝
var a = { age: 27 };
var b = JSON.parse(JSON.stringify(a));
b.age = 29;
a.age // 27
b.age // 29
函式傳引數是按值傳遞?按引用傳遞?
var person = {
age: 27
};
function foo (person) {
person.age = 29;
}
foo(person);
person.age // 29;
函式呼叫時,會對引數賦值。而引數傳遞過程其實同樣是變數複製的過程,所以它是按值傳遞。var person = person
,因為傳遞引數是物件時,變數複製僅複製的棧儲存(淺拷貝),所以修改物件屬性會造成外部變數物件的修改。
至此,當我們理清棧、堆資料結構,以及JS中資料型別存取方式。深淺拷貝問題也就通順了。
記憶體空間
記憶體空間管理
JavaScript執行過程中記憶體分配:
- 為變數物件分配需要的記憶體
- 在分配到的記憶體中進行讀/寫操作
- 不再使用時將其銷燬,釋放記憶體
記憶體管理不善,會出現記憶體洩露,造成瀏覽器記憶體佔用過多,頁面卡頓等問題。
垃圾回收機制
JavaScript中有自動垃圾回收機制,會通過標記清除的演算法識別哪些變數物件不再使用,對其進行銷燬。開發者也可在程式碼中手動設定變數值為null(a = null
)進行標記清除,讓其失去引用,以便下一次垃圾回收時進行有效回收。
區域性環境中,函式執行完成後,函式區域性環境宣告的變數不再需要時,就會被垃圾回收銷燬(理想的情況下,閉包會阻止這一過程)。
全域性環境只有頁面退出時才會出棧,解除變數引用。所以開發者應儘量避免在全域性環境中建立全域性變數,如需使用,也要在不需要時手動標記清除,將其記憶體釋放掉。
為什麼會有棧記憶體和堆記憶體之分?
通常與垃圾回收機制有關。為了使程式執行時佔用的記憶體最小。
當一個方法執行時,每個方法都會建立自己的記憶體棧,在這個方法內定義的變數將會逐個放入這塊棧記憶體裡,隨著方法的執行結束,這個方法的記憶體棧也將自然銷燬了。因此,所有在方法中定義的變數都是放在棧記憶體中的;
當我們在程式中建立一個物件時,這個物件將被儲存到執行時資料區中,以便反覆利用(因為物件的建立成本通常較大),這個執行時資料區就是堆記憶體。堆記憶體中的物件不會隨方法的結束而銷燬,即使方法結束後,這個物件還可能被另一個引用變數所引用(方法的引數傳遞時很常見),則這個物件依然不會被銷燬,只有當一個物件沒有任何引用變數引用它時,系統的垃圾回收機制才會在核實的時候回收它。