js運行機制及異步編程(一)

相信你們在面試的過程當中常常遇到查看執行順序的問題,如setTimeout,promise,async await等等,各類組合,是否是感受頭都要暈掉了,其實這些問題最終仍是考察你們對js的運行機制是否掌握牢固,對promise,async的原理是否掌握,萬變不離其宗,此次就來完全搞懂它。

1 js引擎的運行原理

js引擎也是程序,是屬於瀏覽器的一部分,由瀏覽器廠商自行開發。從頭至尾負責整個JavaScript程序的編譯及執行過程面試

瀏覽器在渲染的過程當中,首先按順序加載由<script>標籤分割的js代碼塊,加載js代碼塊完畢後,須要js引擎進行解析。不管是外部腳本文件(不異步加載)仍是內部腳本代碼塊,都是同樣的原理,而且都在同一個全局做用域中。

JavaScript被歸類爲「動態」或「解釋執行」語言,因此它無需提早編譯,而是由解釋器實時運行chrome

js引擎執行過程分爲三個階段:編程

  • JS的解釋階段
  • JS的預處理(編譯)階段及執行階段

1.1 JS的解釋階段

js腳本代碼塊加載完畢後,會首先JS的解釋階段。該階段主要過程以下:數組

  1. 詞法分析——這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)
  2. 語法分析——這個過程是將詞法單元流(數組)轉化成抽象語法樹(Abstract Syntax Tree)
  3. 使用翻譯器(translator),將代碼轉爲字節碼(bytecode)
  4. 使用字節碼解釋器(bytecode interpreter),將字節碼轉爲機器碼

最終計算機執行的就是機器碼。promise

爲了提升運行速度,現代瀏覽器通常採用即時編譯(JIT-Just In Time compiler)瀏覽器

即字節碼只在運行時編譯,用到哪一行就編譯哪一行,而且把編譯結果緩存(inline cache)緩存

這樣整個程序的運行速度能獲得顯著提高。安全

並且,不一樣瀏覽器策略可能還不一樣,有的瀏覽器就省略了字節碼的翻譯步驟,直接轉爲機器碼(如chrome的v8)閉包

1.2 JS的預處理(編譯)階段及執行階段

這裏我理解爲js爲解釋型語言,由解釋器實時運行,通俗的說就是預處理完以後立刻執行,一邊編譯一邊執行

1.2.1 js的執行環境主要有三種:

  1. 全局環境
  2. 函數環境
  3. eval(不建議使用,會有安全,性能問題)

1.2.2 如下段例子說明js的預編譯與執行過程

function bar() {
    var B_context = "Bar EC";

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

    foo()
}

bar()

這段函數通過詞法解析,語法解析階段以後,就開始進入預編譯並執行,以下:異步

clipboard.png

  1. 首先,進入全局環境,就會先進行預處理,然建立全局上下文執行環境(Global ExecutionContext),會對var聲明的變量和函數聲明進行預處理,window對象就是全局執行上下文的變量對象,全部的變量和函數都是window對象的屬性方法。因此函數聲明提早和變量聲明提高是在建立變量對象中進行的,且函數聲明優先級高於變量聲明。而後推入stack棧中。預處完成以後,開始執行js
  2. 當執行bar()時,就會進入bar函數運行環境,就會先進行預處理,建立bar函數執行上下文(bar Execution Context),推入stack棧中,預處理完後,開始執行foo()
  3. 在bar函數內部調用foo函數,則再進入foo函數運行環境,建立foo函數執行上下文(foo Execution Context),推入stack棧中
  4. 此刻棧底是全局執行上下文(Global Execution Context),棧頂是foo函數執行上下文(foo Execution Context),如上圖,因爲foo函數內部沒有再調用其餘函數,那麼則開始出棧
  5. foo函數執行完畢後,棧頂foo函數執行上下文(foo Execution Context)首先出棧
  6. bar函數執行完畢,bar函數執行上下文(bar Execution Context)出棧
  7. Global Execution Context則在瀏覽器或者該標籤頁關閉時出棧。

1.2.3 執行上下文

clipboard.png

分析一段簡單的代碼,幫助咱們理解建立執行上下文的過程,以下:

function fun(a, b) {
    var num = 1;

    function test() {

        console.log(num)

    }
}

fun(2, 3)

這裏咱們在全局環境調用fun函數,建立fun執行上下文,這裏爲了方便你們理解,暫時不講解做用域鏈以及this指向,以下:

funEC = {
    //變量對象
    VO: {
        //arguments對象
        arguments: {
            a: undefined,
            b: undefined,
            length: 2
        },

        //test函數
        test: <test reference>, 

        //num變量
        num: undefined
    },

    //做用域鏈
    scopeChain:[],

    //this指向
    this: window
}
  • funEC表示fun函數的執行上下文(fun Execution Context簡寫爲funEC)
  • funE的變量對象中arguments屬性,上面的寫法僅爲了方便你們理解,可是在瀏覽器中展現是以類數組的方式展現的
  • <test reference>表示test函數在堆內存地址的引用
注:建立變量對象發生在預編譯階段,但還沒有進入執行階段,該變量對象都是不能訪問的,由於此時的變量對象中的變量屬性還沒有賦值,值仍爲undefined,只有進入執行階段,變量對象中的變量屬性進行賦值後,變量對象(Variable
Object)轉爲活動對象(Active Object)後,才能進行訪問,這個過程就是VO –> AO過程。

創建做用域鏈
做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

理清做用域鏈能夠幫助咱們理解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
}

在這裏咱們順便思考一下,什麼是閉包?
咱們先看下面一個簡單例子,以下:

function foo() {
    var num = 20;

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

        return result
    }

    bar()
}

foo()

我這裏直接以瀏覽器解析,以瀏覽器理解的閉包爲準來分析閉包,以下圖:

clipboard.png

如上圖所示,chrome瀏覽器理解閉包是foo,那麼按瀏覽器的標準是如何定義閉包的,我總結爲三點:

  • 在函數內部定義新函數
  • 新函數訪問外層函數的局部變量,即訪問外層函數環境的活動對象屬性
  • 新函數執行,建立新的函數執行上下文,外層函數即爲閉包

肯定this指向在全局環境下,全局執行上下文中變量對象的this屬性指向爲window;函數環境下的this指向卻較爲靈活,需根據執行環境和執行方法肯定

相關文章
相關標籤/搜索