原生JS大揭祕—JS代碼執行原理解刨

JavaScript是一種基於對象的動態、弱類型腳本語言(如下簡稱JS),是一種解釋型語言,和其餘的編程語言不一樣,如java/C++等編譯型語言,這些語言在代碼執行前會進行通篇編譯,先編譯成字節碼(機器碼)。而後在執行。而JS不是這樣作的,JS是不須要編譯成中間碼,而是能夠直接在瀏覽器中運行,JS運行過程可分爲兩個階段,編譯和執行。(可參考你不知道的JS這本書),當JS控制器轉到一段可執行的代碼時(這段可執行代碼就是編譯階段生成的),會建立與之對應的執行上下文(Excution Context簡稱EC)。執行上下文能夠理解爲執行環境(執行上下文只能由JS解釋器建立,也只能由JS解釋器使用,用戶是不能夠操做該"對象"的)。

JS中的執行環境分爲三類:

  • 全局環境:當JS引擎進入一個代碼塊時,如遇到<script>xxx</script>標籤,就是進入一個全局執行環境
  • 函數環境:當一個函數被調用時,在函數內部就造成了一個函數執行環境
  • eval():把字符串單作JS代碼執行,不推薦使用

在一段JS代碼中可能會產生多個執行上下文,在JS中用棧這種數據結構來管理執行上下文,棧的特色是「先進後出,後進先出」,這種棧稱之爲執行上下文棧(Excution Context Stack 簡稱ECS)。

執行上下文的特色

  • 棧底永遠是全局執行上下文,有且僅有一個
  • 全局執行上下文只有在瀏覽器關閉時,纔會彈出棧
  • 其餘的執行上下文的數量沒有限制
  • 棧頂永遠是當前活動執行上下文,其他的都處於等待狀態中,一旦執行完畢,當即彈出棧,而後控制權交回下一個執行上下文
  • 函數只有在每次被調用時,纔會爲其建立執行上下文,函數被聲明時是沒有的。

有關VO、GO、AO區別

  • 全局 VO -> GO
  • 函數 VO -> AO

執行上下文能夠形象的理解爲一個普通的JS對象,一個執行上下文的生命週期大概包含兩個階段:

  • 建立階段

    此階段主要完成三件事件,一、建立變量對象 二、創建做用域鏈 三、肯定this指向javascript

  • 執行階段

    此階段主要完成變量賦值、函數調用、其餘操做java

變量對象(VO)的建立過程

  • 一、根據函數參數,建立並初始化arguments對象,編程

    AO={
        //建立arguments對象,注意這裏尚未添加任何屬性
        arguments:{
            length:0 //設置初始值爲0
        }
    }
  • 二、查找function函數聲明,在變量對象上添加屬性,屬性名就是函數名,屬性值就是函數的引用值,若是已經存在同名的,則直接覆蓋
  • 三、查找var變量聲明(查找變量時,會把函數的參數等價於var聲明,因此在AO中也會添加和參數名同樣的屬性,初始值也是undefined),在變量對象添加屬性,屬性名就是變量名,屬性值是undefined,若是已經存在同名的,則不處理
    查找var變量聲明時,是從函數的參數開始的,此時函數的參數定義至關於以下代碼數組

    Foo(a, b, c){
           //-------------------這一部分是隱藏的咱們沒法顯示看到-------------------------
           var a=1; //傳入參數
           var b=2; //傳入參數
           var c; //沒有傳入參數
           //--------------------------------------------
           var name="kity"
             
           function bar(){}
           
       }
       
       Foo(11,22)

若是存在同名標識符(函數、變量),則函數能夠覆蓋變量,函數的優先級高於變量

變量對象(OV)激活對象(AO)是同一個東西,在全局執行上下文中是VO,在函數執行上下文中是AO,由於VO在全局執行上下文中能夠直接訪問,可是在函數執行上下文中是不能直接訪問的,此時有AO扮演VO的角色

以以下代碼爲例瀏覽器

var g_name="tom";
var g_age=20;
function g_fn(num){
    var l_name="kity";
    var l_age=18;
    function l_fn(){
        console.log(g_name + '===' + l_name + '===' + num);
    }
}
g_fn(10);

編譯階段

當JS控制器轉到這一段代碼時,會建立一個執行上下文,G_EC
執行上下文的結構大概以下:數據結構

G_EC = {
    VO          : {},
    Scope_chain : [],
    this        : {}
}

/* VO的結構大概 */
VO = {
    g_name : undefined,
    g_age  : undefined,
    g_fn   : <函數在內存中引用值>
}

/* Scope_chain的大概結構以下 */
Scope_chain = [ G_EC.VO ] // 數組中第一個元素是當前執行上下文的VO,第二個是父執行上下文的VO,最後一個是全局執行上下文的VO,在執行階段,會沿着這個做用域鏈一個一個的查找標識符,若是查到則返回,否知一直查找到全局執行上下文的VO

/* this */
this = undefined // 此時this的值是undefined

執行上下文一旦建立完畢,就立馬被壓入函數調用棧中,此時解釋器會悄悄的作一件事情,就是給當前VO中的函數添加一個內部屬性[[scope]],該屬性指向上面的做用域鏈。編程語言

g_fn.scope = [ global_EC.VO ] // 該scope屬性只能被JS解釋器所使用,用戶沒法使用

執行階段(此階段VO->AO

一行一行執行代碼,當遇到一個表達式時,就會去當前做用域鏈的中查找VO對象,若是找到則返回,若是找不到,則繼續查找下一個VO對象,直至全局VO對象終止。
此階段能夠有變量賦值,函數調用等操做,當解釋器遇到g_fn()時,就知道這是一個函數調用,而後當即爲其建立一個函數執行上下文,fn_EC,該上下文fn_EC一樣有兩個階段
分別是建立階段和執行階段。
在建立階段,對於函數的執行上下文,首先會從函數的參數能夠逐行執行,函數

  • 先在AO中添加一個屬性arguments

AO={測試

arguments:{
   length:0
}

}this

  • 查找function函數聲明
  • 查找var變量聲明(在查找var變量時,並無向arguments中注入實參,實參是在執行階段注入的,由於arguments處理的是實參,這個時候尚未涉及到實參值)

特別說明:執行階段,當函數參數執行賦值操做時,若是AO中有同名的函數存在,則直接跳過,同時會將arguments對象中相應的屬性值修改爲該函數值,這是一個坑啊

function test(a, b) {

    console.log(a);//1

    c = 0;

    var c;

    a = 3;
    b = 2;

    console.log(b);//2

    function b() { }
    function d() { }
    console.log(b);//2
}
test(1)

關於全局執行上下文的VO對象分析:

clipboard.png
各個瀏覽器測試狀況以下:

clipboard.png

clipboard.png

clipboard.png

相關文章
相關標籤/搜索