JavaScript 到底是如何工做的?(第二部分)

照片來源於Unsplash上的Samuel Zellerjavascript

這篇文章的第一部分,我簡要概述了編程語言的通常工做機制,並深刻探討了 V8 引擎的管道。第二部分將介紹一些更重要的概念,這些概念是每個 JavaScript 程序員都必須瞭解的,而且不只僅和 V8 引擎有關。
對於任何一個程序員來講,最關注的兩個問題無非就是:時間複雜度空間複雜度第一部分介紹了 V8 爲改進 JavaScript 執行時間所作的速度提高和優化,第二部分則將着重介紹內存管理方面的知識。java

內存堆

Orinoco 的 logo:V8 的垃圾回收器git

  • 每當你在 JavaScript 程序中定義了一個變量、常量或者對象時,你都須要一個地方來存儲它。這個地方就是內存堆。
  • 當遇到語句 var a = 10 的時候,內存會分配一個位置用於存儲 a 的值
  • 可用內存是有限的,而複雜的程序可能有不少變量和嵌套對象,所以合理地使用可用內存很是重要。
  • 和諸如 C 這種須要顯式分配和釋放內存的語言不一樣,JavaScript 提供了自動垃圾回收機制。一旦對象/變量離開了上下文而且再也不使用,它的內存就會被回收並返還到可用內存池中。
  • 在 V8 中,垃圾回收器的名字叫作 Orinoco,它的處理過程很是高效。這篇文章有相關解釋。

標記與清除算法

標記和清除算法程序員

咱們一般會使用這種簡單有效的算法來斷定能夠從內存堆中安全清除的對象。算法的工做方式正如其名:將對象標記爲可得到/不可得到,並將不可得到的對象清除。
垃圾回收器週期性地從根部或者全局對象開始,移向被它們引用的對象,接着再移向被這些對象引用的對象,以此類推。全部不可得到的對象會在以後被清除。github

內存泄漏

雖然垃圾回收器很高效,可是開發者不該該就此將內存管理的問題束之高閣。管理內存是一個很複雜的過程,哪一塊內存再也不須要並非單憑一個算法就能決定的。
內存泄漏指的是,程序以前須要用到部份內存,而這部份內存在用完以後並無返回到內存池。
下面是一些會致使你的程序出現內存泄漏的常見錯誤:
全局變量:若是你不斷地建立全局變量,無論有沒有用到它們,它們都將滯留在程序的整個執行過程當中。若是這些變量是深層嵌套對象,將會浪費大量內存。算法

var a = { ... }
var b = { ... }
function hello() {
  c = a;  // 這是一個你沒有意識到的全局變量
}

若是你試圖訪問一個此前沒有聲明過的變量,那麼將在全局做用域中建立一個變量。在上面的例子中,c 是沒有使用 var 關鍵字顯式建立的變量/對象。chrome

事件監聽器:爲了加強網站的交互性或者是製做一些浮華的動畫,你可能會建立大量的事件監聽器。而用戶在你的單頁面應用中移向其餘頁面時,你又忘記移除這些監聽器,那麼也可能會致使內存泄漏。當用戶在這些頁面來回移動的時候,這些監聽器會不斷增長。編程

var element  = document.getElementById('button');
element.addEventListener('click', onClick)

Intervals 和 Timeouts:當在這些閉包中引用對象時,除非閉包自己被清除,不然不會清除相關對象。瀏覽器

setInterval(() => {
  // 引用對象
}
// 這時候忘記清除計時器
// 那麼將致使內存泄漏!

移除 DOM 元素:這個問題很常見,相似於全局變量致使的內存泄漏。DOM 元素存在於對象圖內存和 DOM 樹中。用例子來解釋可能會更好:安全

var terminator = document.getElementById('terminate');
var badElem = document.getElementById('toDelete');
terminator.addEventListener('click', function()  {memory
  badElem.remove();
});

在你經過 id = ‘terminate’ 點擊了按鈕以後,toDelete 會從 DOM 中移除。不過,因爲它仍然被監聽器引用,爲這個對象分配的內存並不會被釋放。

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"

1.引擎瞭解到咱們的程序中有兩個函數
2.運行 calculate() 函數
3.將 calculate 壓棧並計算兩數之和
4.運行 multiplyByTwo() 函數
5.將 multiplyByTwo 函數壓棧並執行算術計算 x*2
6.在返回結果的同時,將 multiplyByTwo() 從棧中彈出,以後回到 calculate() 函數
7.在 calculate() 函數返回結果的同時,將 calculate() 從棧中彈出,繼續執行後面的代碼

棧溢出

在不對棧執行彈出的狀況下,可連續壓棧的數目取決於棧的大小。若是超過了這個界限以後還不斷地壓棧,最終會致使棧溢出。chrome 瀏覽器將會拋出一個錯誤以及被稱爲棧幀的棧快照。

遞歸:遞歸指的是函數調用自身。遞歸能夠大幅度地減小執行算法所花費的時間(時間複雜度),不過它的理解和實施較爲複雜。
下面的例子中,基本事件永遠不會執行,lonley 函數在沒有返回值的狀況下不斷地調用自身,最終會致使棧溢出。

function lonely() {
 if (false) {
  return 1;  // 基本事件
 }
 lonely();   // 遞歸調用
}

爲何 JavaScript 是單線程的?

一個線程表明着在同一時間段內能夠單獨執行的程序部分的數目。要想查看一門語言是單線程的仍是多線程的,最簡單的方式就是了解它有多少個調用棧。JS 只有一個,因此它是單線程語言。
這樣不是會阻礙程序運行嗎?若是我運行多個耗時的阻塞操做,例如 HTTP 請求,那麼程序必須得在每個操做獲得響應以後才能執行後面的代碼。
爲了解決這個問題,咱們須要找到一種能夠在單線程下異步完成任務的辦法。事件循環就是用來發揮這個做用的。

事件循環

到如今爲止,咱們談到的內容大多包含在 V8 裏面,可是若是你去查看 V8 的代碼庫,你會發現它並不包含例如 setTimeout 或者 DOM 的實現。事實上,除了運行引擎以外,JS 還包括瀏覽器提供的 Web API,這些 API 用於拓展 JS。
關於事件循環的概念,菲利普·羅伯茨講得比我更好,能夠看下面這段視頻:
http://player.youku.com/embed...

結論

關於製做一門編程語言,其實還有不少內容,而且語言的實如今這些年也是不斷變化的。我但願這兩篇博客能夠幫助你成爲一名更好的 JS 程序員,而且接受 JS 中那些晦澀難懂的內容 。對於諸如「V8」,「事件循環」,「調用棧」這樣的術語,你如今應該熟悉了。大部分的學生(好比我)是從一個新的框架起步,以後再去學習原生 JS。如今他們應該熟悉代碼背後發生的事情了,反過來,這將幫助他們寫出更好的代碼。

相關文章
相關標籤/搜索