javascript引擎執行的過程的理解--語法分析和預編譯階段

saucxs發表於2019-01-08

一、概述

js是一種非常靈活的語言,理解js引擎的執行過程對於我們學習js是非常有必要的。看了很多這方便文章,大多數是講的是事件迴圈(event loop)或者變數提升的等,並沒有全面分析其中的過程。所以覺得把這個js執行的詳細過程整理一下,幫助更好的理解js。

1.1基礎概念

js是單執行緒語言。

在瀏覽器中一個頁面永遠只有一個執行緒在執行js指令碼程式碼

js是單執行緒怨言,但是程式碼解析是非常迅速的,不會發生解析阻塞。

js是非同步執行的,通過實踐迴圈(event loop)方式實現的


暫時我們不考慮事件迴圈(event loop),我們先來看這樣一段程式碼,來確定我們是否理解js引擎的執行過程

console.log(person)

console.log(personFun)

var person = "saucxs";

console.log(person)

function personFun() {
    console.log(person)
    var person = "songEagle";
    console.log(person)
}

personFun()

console.log(person)複製程式碼

可以自己直接使用瀏覽器看出輸出結果

首先我們來分析一下上面的程式碼,雖然很多開發人員基本上都能答出來,但是還是要囉嗦一下。

全面分析js引擎的執行過程,分為三個階段

1、語法分析

2、預編譯階段

3、執行階段

說明:瀏覽器先按照js的順序載入<script>標籤分隔的程式碼塊,js程式碼塊載入完畢之後,立刻進入到上面的三個階段,然後再按照順序找下一個程式碼塊,再繼續執行三個階段,無論是外部指令碼檔案(不非同步載入)還是內部指令碼程式碼塊,都是一樣的,並且都在同一個全域性作用域中。


二、語法分析

js的程式碼塊載入完畢之後,會首先進入到語法分析階段,該階段的主要作用:

分析該js指令碼程式碼塊的語法是否正確,如果出現不正確會向外丟擲一個語法錯誤(syntaxError),停止改js程式碼的執行,然後繼續查詢並載入下一個程式碼塊;如果語法正確,則進入到預編譯階段。

類似的語法報錯的如下圖所示:



三、預編譯階段

js程式碼塊通過語法分析階段之後,語法都正確的下回進入預編譯階段。

在分析預編譯階段之前,我們先來了解一下js的執行環境,執行環境主要由三種:

1、全域性環境(js程式碼載入完畢後,進入到預編譯也就是進入到全域性環境)

2、函式環境(函式呼叫的時候,進入到該函式環境,不同的函式,函式環境不同)

3、eval環境(不建議使用,存在安全、效能問題)


每進入到一個不同的執行環境都會建立 一個相應的執行上下文(execution context),那麼在一段js程式中一般都會建立多個執行上下文,js引擎會以棧的資料結構對這些執行進行處理,形成函式呼叫棧(call stack),棧底永遠是全域性執行上下文(global execution context),棧頂則永遠時當前的執行上下文。


3.1函式呼叫棧

什麼是函式呼叫棧?

函式呼叫棧就是使用棧存取的方式進行管理執行環境,特點是先進後出,後進後出

我們來分析一下簡答的js程式碼來理解函式呼叫棧:

function bar() {
    var B_context = "bar saucxs";

    function foo() {
        var f_context = "foo saucxs";
    }

    foo()
}

bar()複製程式碼

上面程式碼塊通過語法分析後,進入預編譯階段,如圖所示

stack

1、首先進入到全域性環境,建立全域性執行上下文(global Execution Context ),推入到stack中;

2、呼叫bar函式,進入bar函式執行環境,建立bar函式執行上下文(bar Execution Context),推入stack棧中;

3、在bar函式內部呼叫foo函式,則再進入到foo函式執行環境中,建立foo函式執行上下文(foo Execution Context),如上圖,由於foo函式內部沒有再呼叫其他函式,那麼則開始出棧;

5、foo函式執行完畢之後,棧頂foo函式執行上下文(foo Execution Context)首先出棧;

6、bar函式執行完畢,bar函式執行上下文(bar Execution Context)出棧;

7、全域性上下文(global Execution Cntext)在瀏覽器或者該標籤關閉的時候出棧。

說明:不同的執行環境執行都會進入到程式碼預編譯和執行兩個階段,語法分析則在程式碼塊載入完畢時統一檢查語法。


3.2建立執行上下文

執行上下文可以理解成當前的執行環境,與該執行環境相對應。建立執行上下文的過程中,主要是做了下面三件事,如圖所示:

EC

1、建立變數物件(variable object)

2、建立作用域鏈(scope chain)

3、確定this的指向


3.2.1建立變數物件

建立變數物件主要是經過以下過程,如圖所示:

VO

1、建立arguments物件,檢查當前上下文的引數,建立該物件的屬性與屬性值,僅在函式環境(非箭頭函式)中進行的,全域性環境沒有此過程。

2、檢查當前上下文的函式宣告,按照程式碼順序查詢,將找到的函式提前宣告,如果當前上下文的變數物件沒有該函式名屬性,則在該變數物件以函式名建立一個屬性,屬性值則指向該函式所在堆記憶體地址引用,如果存在,則會被新的引用覆蓋掉。

3、檢查當前上下文的變數宣告,愛去哪找程式碼順序查詢,將找到的變數提前宣告,如果當前上下文的變數物件沒有變數名屬性,則在該變數物件以變數名建立一個屬性,屬性值為undefined;如果存在,則忽略該變數宣告。

說明:在全域性環境中,window物件就是全域性執行上下文的變數物件,所有的變數和函式都是window物件的屬性方法。

所以函式宣告提前和變數宣告提升是在建立變數物件中進行的,且函式宣告優先順序高於變數宣告。

下面我們再來分析這個簡單程式碼

function fun(m,n){
    var saucxs = 1;

    function execution(){
        console.log(saucxs)
    }
}

fun(2,3)複製程式碼

這裡我們在全域性環境中呼叫fun函式,建立fun的執行上下文,這裡暫時不說作用域鏈以及this指向的問題。

funEC = {
    //變數物件
    VO: {
        //arguments物件
        arguments: {
            m: undefined,
            n: undefined,
            length: 2
        },

        //execution函式
        execution: <execution reference>, 

        //num變數
        saucxs: undefined
    },

    //作用域鏈
    scopeChain:[],

    //this指向
    this: window
}複製程式碼

1、funEC表示fun函式的執行上下文(fun Execution Context 簡寫為funEC);

2、funEC的變數物件中arguments屬性,上面這樣寫只是為了理解,在瀏覽器中展示以類陣列的方式展示的

3、<execution reference>表示的是execution函式在堆記憶體地址的引用

說明:建立變數物件發生在預編譯階段,還沒有進入到執行階段,該變數物件都不能訪問的,因為此時的變數物件中的變數屬性尚未賦值,值仍為undefined,只有在進行執行階段,變數中的變數屬性才進行賦值後,變數物件(Variable Object)轉為活動物件(Active Object)後,才能進行訪問,這個過程就是VO->AO過程。


3.2.2建立作用域鏈

作用域鏈由當前執行環境的變數物件(未進入到執行階段前)與上層環境的一系列活動物件組成,保證了當前執行還款對符合訪問許可權的變數和函式有序訪問。

理解清楚作用域鏈可以幫助我們理解js很多問題包括閉包問題等,下面我們結合一個例子來理解一下作用域鏈。

var num = 30;

function test() {
    var a = 10;

    function innerTest() {
        var b = 20;

        return a + b
    }

    innerTest()
}

test()複製程式碼

在上面例子中,當執行到呼叫innerTest函式,進入到innerTest函式環境。全域性執行上下文和test函式執行上下文已進入到執行階段,innerTest函式執行上下文在預編譯階段建立變數物件,所以他們的活動物件和變數物件分別是AO(global),AO(test)和VO(innerTest),而innerTest的作用域鏈由當前執行環境的變數物件(未進入到執行階段前)與上層環境的一系列活動物件組成,如下:

innerTestEC = {

    //變數物件
    VO: {b: undefined}, 

    //作用域鏈
    scopeChain: [VO(innerTest), AO(test), AO(global)],  
    
    //this指向
    this: window
}複製程式碼

我們這裡可以直接使用陣列表示作用域鏈,作用域鏈的活動物件或者變數物件可以直接理解成作用域。

1、作用域鏈的第一項永遠是當前作用域(當前上下文的變數物件或者活動物件);

2、最後一項永遠是全域性作用域(全域性上下文的活動物件);

3、作用域鏈保證了變數和函式的有序訪問,查詢方式是沿著作用域鏈從左至右查詢變數或者函式,找到則會停止找,找不到則一直查詢全域性作用域,再找不到就會排除錯誤。


3.2.3閉包

什麼是閉包?思考一下

看一下簡單的例子

function foo() {
    var num = 20;

    function bar() {
        var result = num + 20;

        return result
    }

    bar()
}

foo()複製程式碼

因為對於閉包的有很多的不同理解,包括我看一些書籍(js高階程式設計),我這直接以瀏覽器解析,以瀏覽器的閉包為準來分析閉包,如圖


如圖所示,谷歌瀏覽器理解的閉包是foo,那麼按照瀏覽器的標準是如何定義的閉包,自己總結為三點:

1、在函式內部定義新函式

2、新函式訪問外層函式的區域性變數,即訪問外層函式環境的活動物件屬性

3、新函式執行,建立新函式的執行上下文,外層函式即為閉包


3.2.4確定this指向

1、在全域性環境下,全域性執行的上下文中變數物件的this屬性指向為window;

2、在函式環境下的this指向比較靈活,需要根據執行環境和執行方法確定,列舉典型例子來分析


四、總結

由於涉及到的內容過多,下一次將第三階段(執行階段)單獨分離出來。另開出新文章詳細分析,主要介紹js執行階段中的同步任務執行和非同步任務執行機制(事件迴圈(Event Loop))。


五、參考書籍

  • 你不知道的javascript(上卷)


相關文章