你的JavaScript代碼都經歷了什麼

從語言類型提及

要知道你寫的代碼接下來是交給誰的,先要明白解釋型語言和編譯型語言。html

解釋型語言:這種類型的編程語言,會將代碼一句一句直接運行,不須要像編譯語言(Compiled language)同樣,通過編譯器先行編譯爲機器碼,以後再運行。這種編程語言須要利用解釋器,在運行期,動態將代碼逐句解釋(interpret)爲機器碼,或是已經預先編譯爲機器碼的的子程序,以後再運行。html5

編譯型語言:是一種以編譯器來實現的編程語言。它不像解釋型語言同樣,由解釋器將代碼一句一句運行,而是以編譯器,先將代碼編譯爲機器碼,再加以運行。理論上,任何編程語言均可以是編譯式,或直譯式的。它們之間的區別,僅與程序的應用有關。算法

那麼,JavaScript就是典型的解釋型語言,那麼要運行JavaScript程序就必需要有響應的執行環境,也就是要經過JavaScript引擎解析執行JS代碼。JavaScript引擎的基本工做是把開發人員寫的JavaScript代碼轉換成高效、優化的代碼,這樣就能夠經過瀏覽器進行解釋甚至嵌入到應用中。好比著名的V8引擎。chrome

JavaScript的解析過程分爲兩個階段:預編譯期(預處理)執行期。在預編譯期,JavaScript解釋器完成對JavaScript代碼的預處理,轉換爲字節碼。執行期間,JavaScript解釋器把字節碼轉換成二進制碼,按照順序執行。編程

預編譯期:

正常的編譯型語言編譯期,其過程可分爲6步:詞法分析、語法分析、語義分析、源代碼優化、代碼生成、目標代碼優化。對於JavaScript來講,經過詞法分析和語法分析獲得語法樹後,就會進入到執行期,執行代碼。瀏覽器

詞法分析:在詞法分析階段,JavaScript解釋器先把代碼的字符流轉換爲記號流,例如:bash

a = (b -c)
複製代碼

轉換爲記號流:數據結構

NAME "a"  
EQUALS  
OPEN_PARENTHESIS  
NAME "b"  
MINUS  
NAME "c"  
CLOSE_PARENTHESIS  
SEMICOLON 
複製代碼

詞法分析階段能夠實現的是:一、去掉註釋,生成文檔;二、記錄錯誤信息;三、完成預處理多線程

語法分析:

語法分析階段就是把詞法分析階段產生的記號,生成語法樹,即把從程序中收集的信息存儲到數據結構中,數據結構在此處爲兩種:一、符號表:記錄變量、函數、類;二、語法樹:程序結構的樹形表示,將此樹形結構生成中間代碼。例如:閉包

if(typeof a == "undefined" ) { 
    a = 0
 } else { 
    a = a
 } 
 alert(a)
複製代碼

生成的語法樹爲:

當構建語法樹的過程當中,沒法構造,則報出語法錯誤,並結束整個代碼塊的解析。 詞法分析和語法分析階段是交錯進行的,每取一個詞法記號,就送入語法分析器進行分析。 詞法、語法分析是有規則的,其中ECMAScript262這份文檔,就是對JavaScript這門語言定義了一整套完整的標準。語法分析就依靠這套標準,固然也有不按照標準來實現的,好比IE的JS引擎。這也是爲何JavaScript會有兼容性的問題。

執行期

通過編譯階段的準備,代碼在內存中已經構建成語法樹,JavaScript引擎會根據這個語法樹結構邊解釋邊執行。解釋過程當中,引擎嚴格按照做用域機制執行。JavaScript採用的詞法做用域,簡單說就是變量和函數的做用域在定義時決定,取決於源代碼結構。

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar();
複製代碼

就像這段代碼,並不會像動態做用域同樣,輸出2. 引擎解釋執行每一個函數時,先建立一個執行環境,在這個環境中建立一個調用對象,這個對象內存儲着當前域中全部局部變量、參數、嵌套函數、引用函數和父級列表。調用對象聲明週期與函數一致,當函數調用完畢且沒有外部引用的狀況下,被垃圾回收機制回收。

同時解釋器經過做用域鏈把多個嵌套的做用域串在一塊兒,並藉助這個鏈,由內而外查找變量值,直到全局對象,若是沒有找到,返回"undefined"。做用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

閉包

在執行環境建立的過程當中,會有一個特殊的狀況——閉包。它由兩部分組成。執行上下文(代號A),以及在該執行上下文中建立的函數(代號B)。當B執行時,若是訪問了A中變量對象中的值,那麼閉包就會產生。在大多數理解中,包括許多著名的書籍,文章裏都以函數B的名字代指這裏生成的閉包。而在chrome中,則以執行上下文A的函數名代指閉包。

function A() {
    var a = 20;
    var b = 30;
    function B() {
        return a + b;
    }
    return B;
}
var B = A();
B();
複製代碼

首先有執行上下文A,在A中定義了函數B,而經過對外返回B的方式讓B得以執行。當B執行時,訪問了A內部的變量a,b。所以這個時候閉包產生。JavaScript擁有自動的垃圾回收機制,關於垃圾回收機制,有一個重要的行爲,那就是,當一個值,在內存中失去引用時,垃圾回收機制會根據特殊的算法找到它,並將其回收,釋放內存。正常來說,當A執行完畢後,生命週期結束,A函數的執行上下文就會失去引用。其佔用的內存空間很快就會被垃圾回收器釋放。但是B函數的存在,會阻止這一過程,使B函數常駐內存。

單線程&&事件循環

JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準? 因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。 爲了利用多核CPU的計算能力,HTML5提出WebWorker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。單個線程使得運行代碼很容易,由於你沒必要處理在多線程環境中出現的複雜場景——例如死鎖。可是在一個線程上運行也很是有限制。因爲JavaScript、只有一個調用堆棧,當某段代碼運行變慢時會發生什麼?

既然是單線程的,在某個特定的時刻只有特定的代碼可以被執行,並阻塞其它的代碼。而瀏覽器是事件驅動的(Event driven),瀏覽器中不少行爲是異步的,會建立事件並放入執行隊列中。JavaScript引擎是單線程處理它的任務隊列。當異步事件發生時,如mouse click, a timer firing, or an XMLHttpRequest completing(鼠標點擊事件發生、定時器觸發事件發生、XMLHttpRequest完成回調觸發等),將他們放入執行隊列,等待當前代碼執行完成再從執行隊列按序拿出事件執行。Event Loop只作一件事情,負責監聽Call Stack和Callback Queue。當Call Stack裏面的調用棧運行完變成空了,Event Loop就把Callback Queue裏面的第一條事件(其實就是回調函數)放到調用棧中並執行它,後續不斷循環執行這個操做。

也就是說JS只有一個調用棧。調用棧是一種數據結構,它記錄了咱們在程序中的位置。若是咱們運行到一個函數,它就會將其放置到棧頂。當從這個函數返回的時候,就會將這個函數從棧頂彈出,這就是調用棧作的做用。棧內的任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。

  • macro-task大概包括:script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)。
  • setTimeout/Promise等咱們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。
setTimeout(function() {
    console.log('xxxx'); // 這段代碼纔是進入任務隊列的任務
})
// setTimeout做爲一個任務分發器,這個函數會當即執行,而它所要分發的任務,也就是它的第一個參數,纔是延遲執行
複製代碼
  • 來自不一樣任務源的任務會進入到不一樣的任務隊列。其中setTimeout與setInterval是同源的。
  • 事件循環的順序,決定了JavaScript代碼的執行順序。它從script(總體代碼)開始第一次循環。以後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),而後執行全部的micro-task。當全部可執行的micro-task執行完畢以後。循環再次從macro-task開始,找到其中一個任務隊列執行完畢,而後再執行全部的micro-task,這樣一直循環下去。
  • 其中每個任務的執行,不管是macro-task仍是micro-task,都是藉助函數調用棧來完成。
垃圾回收

垃圾回收機制有好多種,這裏簡單說下標記清除算法 爲了決定一個對象是否被須要,這個算法用於肯定是否能夠找到某個對象。 其包含如下步驟。

  1. 垃圾回收器生成一個根列表。根一般是將引用保存在代碼中的全局變量。在JavaScript中,window對象是一個能夠做爲根的全局變量。
  2. 全部的根都被檢查和標記成活躍的(不是垃圾),全部的子變量也被遞歸檢查。全部可能從根元素到達的都不被認爲是垃圾。
  3. 全部沒有被標記成活躍的內存都被認爲是垃圾。垃圾回收器就能夠釋放內存而且把內存還給操做系統。 這個算法能夠有效的避免循環依賴問題。
相關文章
相關標籤/搜索