原文地址:How Does JavaScript Really Work? (Part 2)javascript
JavaScript V8 引擎是如何與內存管理,調用堆棧,線程和事件循環協同工做的。java
在上一篇文章中,我簡要概述了編程語言的工做原理及 V8 引擎的細節。 這篇文章將涵蓋每一個 JavaScript 程序員都必須知道的一些重要概念,不只限於 V8 引擎。程序員
時間複雜度和空間複雜度是全部程序員都關注兩個問題。 上一篇文章介紹了 V8 的速度和優化部分,以提升 JavaScript 的執行時間,該部分將重點介紹內存管理方面。web
var a = 10
時,內存將分配一個位置來存儲 a
的值。爲了肯定能夠從內存中安全刪除的對象,使用了這種簡單有效的算法。 該算法的名稱描述了其工做原理;將對象標記爲可訪問/不可訪問,並清除不可訪問的對象。算法
垃圾收集器會按期從根對象或全局對象開始,而後遍歷它們所引用的對象,而後再遍歷這些引用所引用的對象,依此類推。而後清除全部沒法訪問的對象。chrome
儘管垃圾回收是高效的,但這並不意味着開發人員能夠對內存管理無論不顧。 管理內存是一個複雜的過程,肯定哪一塊內存是不須要的不能徹底依賴算法。編程
內存泄漏是指程序使用過的一部份內存,如今再也不使用了,但這些內存並未返回到內存池。瀏覽器
如下是一些致使程序內存泄漏的常見錯誤。安全
全局變量:若是您持續地建立全局變量,即便您不使用它們,它們也會在程序執行過程當中始終存在。若是這些變量是深層嵌套的對象,則會浪費大量內存。bash
var a = { ... }
var b = { ... }
function hello() {
c = a; // this is the global variable that you aren't aware of. } 複製代碼
若是您訪問未聲明的變量,則將在全局範圍內建立一個變量。 在上面的示例中,c
是您沒有使用 var
關鍵字隱式建立的全局變量/全局對象。
Event Listeners: This may happen when you create a lot of event listeners to make your website interactive or maybe just for those flashy animations and forget to remove them when the user moves to some other page in your single page application. Now when the user moves back and forth between these pages, these listeners keep adding up.
事件監聽:假設您在網頁中建立了大量監聽事件來實現交互或動畫,當用戶跳轉到單頁應用程序中的其餘頁面時,而您忘記了移除它們,就可能會發生內存泄漏。 由於當用戶在這些頁面之間來回跳轉時,這些事件監聽會不斷累加。
var element = document.getElementById('button');
element.addEventListener('click', onClick)
複製代碼
定時器:當引用這些閉包中的對象時,垃圾收集器將永遠不會清除被引用的對象,直到閉包自己被清除。
setInterval(() => {
// reference objects
}
// now forget to clear the interval.
// you just created a memory leak!
複製代碼
被刪除的 DOM 節點:有點相似於全局變量內存泄漏,而且很是常見。 DOM 節點存儲在 Object Graph memory 和 DOM tree 中。經過一個示例能夠更好地說明這種狀況。
var terminator = document.getElementById('terminate');
var badElem = document.getElementById('toDelete');
terminator.addEventListener('click', function() {memory
badElem.remove();
});
複製代碼
在點擊 id ='terminate'
的按鈕後,toDelete
節點將從 DOM tree 中刪除。 可是,因爲事件監聽中引用了該對象 badElem
,所以會認爲該對象分配的內存仍在被使用中。
var terminator = document.getElementById('terminate');
terminator.addEventListener('click', function() {
var badElem = document.getElementById('toDelete');
badElem.remove();
});
複製代碼
如今,badElem
變量被定義爲一個局部變量,當刪除操做完成時,垃圾回收器能夠回收它的內存。
堆棧是遵循 LIFO(後進先出)方法來存儲和訪問數據的數據結構。對 JavaScript 引擎來講,堆棧用於記住函數中最後執行的命令的位置。
function multiplyByTwo(x) {
return x*2;
}
function calculate() {
const sum = 4 + 2;
return multiplyByTwo(sum);
}
calculate()
var hello = "some more code follows"
複製代碼
calculate()
函數。calculate
函數並計算總和。multiplyByTwo()
函數。multiplyByTwo
函數,並執行算術運算x * 2。multiplyByTwo()
,而後返回calculate()
函數。calculate()
函數返回時,從堆棧中彈出calculate
,而後繼續執行代碼。在不彈出堆棧的狀況下,連續壓棧量取決於堆棧的大小。 若是您繼續壓棧達到堆棧容量的極限,將致使堆棧溢出,此時 chrome 瀏覽器 會報錯,同時生成堆棧快照,也稱爲堆棧幀
。
遞歸:當函數調用自身時,稱爲遞歸。 在您想減小算法執行的時間(時間複雜度),可是其它方法理解和實現起來很複雜時,遞歸就顯得很是有用。
在下面這個示例中,return 1
語句永遠不會被執行,而且 lonely
函數會不斷調用自身而不會返回,最終致使堆棧溢出。
function lonely() {
if (false) {
return 1; // the base case
}
lonely(); // the recursive call
}
複製代碼
多個線程表示您能夠同時獨立執行程序的多個部分。 肯定一種語言是單線程仍是多線程的最簡單方法是看它擁有有多少個調用堆棧。 JS 只有一個,因此它是單線程語言。
您可能會想這不是瓶頸嗎? 若是我運行多個耗時的操做,也稱爲阻塞操做(如HTTP請求),那麼該程序將必須等待每一個操做的響應完成後,再執行下一個操做。
爲了解決這個問題,咱們須要一種異步執行任務的方法來解決單線程的弊端,事件循環爲此而生。
目前,上面提到的大部份內容都被 V8 囊括,可是若是您在 V8 代碼庫中搜索諸如 setTimeout 或 DOM 之類的內容的實現,那你可能什麼都找不到。由於除了運行時引擎外,JS 還包含 Web API 模塊,這些 API 是瀏覽器提供來擴展 JS 功能的。
您能夠在這個視頻中瞭解箇中詳情。
編寫一門編程語言還有不少工做要作,並且每一年它的實現方式可能也在不斷變化。 我但願這兩篇文章能夠幫助您成爲更好的 JS 程序員,並接納 JS 怪異的部分。 如今,您應該習慣使用V8
,事件循環
,調用堆棧
等術語。
大多數和我同樣人學習 JS 都是從學習一個新的框架開始。 我以爲咱們如今應該對引擎內部執行的東西有一些瞭解,這將有助於咱們編寫出更好的代碼。