JavaScript執行機制深層剖析

prettyEcho發表於2019-02-20

BY 張建成(prettyEcho@github)

除非另行註明,頁面上所有內容採用知識共享-署名(CC BY 2.5 AU)協議共享

原文地址deep.js , 歡迎 評論star

天氣漸漸轉暖了,樹漸漸露出了枝芽,小河也歡快的向前流著,感覺大地充滿了生命力,好開心 ???

附上美圖一張

AST

小夥伴們,我們也出來活動活動筋骨,迎接我們2018年的春天。

今天我們說說JS執行流程,現在我們先暫且不考慮非同步的情況。

如果你把下面的內容都吃透,那你就會發現JS內部是多麼精彩的一個世界。

還等什麼,go...

編譯階段

詞法分析(Lexing)

這個過程會將由字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些代 碼塊被稱為詞法單元(token)。

簡單舉個例子:c = b - a 轉換為

  • NAME "c"
  • EQUALS
  • NAME "a"
  • MINUS
  • NAME "b"
  • SEMICOLON

語法分析(Parsing)

這個過程是將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法 結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。

AST大概是下面的樣子:

AST

生成可執行程式碼

將 AST 轉換為可執行程式碼的過程稱被稱為程式碼生成。

執行階段

接下來,我們以一個簡單例子進行分析。

var a = 2;

function bar() {
    var b = 2;

    function foo() {
        var c = 2;
    }

    foo();
}

bar();
複製程式碼

1. JS引擎建立一個全域性物件(Global Object)

這個物件全域性只存在一份,它的屬性在任何地方都可以訪問,它的存在伴隨著應用程式的整個生命週期。全域性物件在建立時,將Math,String,Date,document 等常用的JS物件作為其屬性。由於這個全域性物件不能通過名字直接訪問,因此還有另外一個屬性window,並將window指向了自身,這樣就可以通過window訪問這個全域性物件了。用虛擬碼模擬全域性物件的大體結構如下:

//建立一個全域性物件
var globalObject = {
    Math:{},
    String:{},
    Date:{},
    document:{}, //DOM操作
    ...
    window:this //讓window屬性指向了自身
}
複製程式碼

2. JS引擎會建立一個執行環境棧(Execution Context Stack)

  • 棧 提到棧,小夥伴們都知道,棧是一種類似羽毛球筒儲存羽毛球的資料結構,採用先進後出,後進先出的特點。

棧

上圖中的羽毛球1一定是先放入棧中,然後是羽毛球2,以此類推,而出棧時,一定是羽毛球5先拿出來,然後是羽毛球4,以此類推,這種方式和棧存取資料的方式如出一轍。

  • 堆 堆資料型別類似與書架。書雖然也整齊的存放在書架上,但是我們只要知道書的名字,我們就可以很方便的取出我們想要的書。

好了好了,扯遠了。我們接著往下說,在這隻需知道執行環境棧是怎樣存取資料的就行。

3. 建立全域性執行上下文(Execution Context)

到這你可能會問,上下文是個啥玩意?

是啊,上下文是個什麼鬼啊?

上下文不是玩意,也不是什麼鬼。

執行上下文可以理解為當前程式碼的執行環境。JS所有程式碼都會在自己的上下文環境下執行。

說到上下文,你可能會有這樣的疑惑:上下文不就是作用域嗎?

老鐵,我肯定的告訴你,上下文不是作用域。的確,在JS裡,這還真是個很難區分的東東。不過現在我還不能馬上道出他們的區別,因為作用域的知識,我們還沒有涉及,?徹底搞懂JavaScript作用域,通過這篇文章,你將徹徹底底瞭解關於作用域的一切。

那在JS中會有幾種執行環境呢?

大概有3種:

  • 全域性環境:JavaScript程式碼執行起來會首先進入該環境
  • 函式環境:當函式被呼叫執行時,會進入當前函式中執行程式碼
  • eval、with(不建議使用,可忽略)

因此在一個JavaScript程式中,必定會產生多個執行上下文。

go on...

4. 全域性上下文推入執行環境棧底

5. 程式碼開始從上往下執行,這裡我們暫且不談識別符號處理,當程式碼執行到bar(),生成bar執行上下文,推入棧中

6. 程式碼執行到foo(),生成foo執行上下文,推入棧中

7. foo()執行完,foo執行上下文出棧

8. bar()執行完,bar執行上下文出棧

9. 全域性上下文執行上下文出棧

我們用圖走一下js執行流程,是這樣的:

flow

小夥伴們,現在是不是對JS執行流程有了一個整體認識,下面我們來說點更有意思的。

上下文執行細節

我們先看整體瞭解下

context-detail

建立階段

1. 建立變數物件(Variable Object)

建立變數物件,依次經歷了以下幾個步驟

  1. 建立arguments物件。檢測當前上下文引數,建立該對物件下的屬性及屬性值。(這裡提一下,函式的引數是按值傳遞,我知道你是知道的)
  2. 檢測關鍵詞function函式宣告。檢測當前上下文中的函式宣告,並掛載到變數物件上,其值是函式物件的引用。
  3. 檢測var變數宣告。檢測當前上下文中的var宣告,並賦值為undefined;如遇到同名var宣告的變數,則會預設覆蓋;如遇到同名函式宣告,則預設忽略,這也就體現了函式宣告的優先順序要高於var宣告。誰的大哥還是得分清的,哈哈。。。
變數提升

看到這,我覺得你對變數提升具體是什麼以及如何實現的應該瞭解的一清二楚了。

是不是呢?

我們來一道題測試下

function foo() {
    console.log(a);
    console.log(baz);

    var a = 'inner';
    var baz = 1;

    function baz() {}
}

foo();
複製程式碼

第一處是undefined,第二處是[Function: baz],是不是很簡單?

下面我用程式碼簡單模擬下上面的過程

function foo() {
    function baz() {}

    var a = undefined;

    console.log(a);
    a = 'inner';

    console.log(baz);
}

foo();

複製程式碼

變數物件大概是這樣的

 VO(foo) = {
     arguments: {},
     baz: <foo reference>,  // 表示foo的地址引用
     a: undefined
 }
複製程式碼

2. 確定作用域鏈

作用域鏈是由當前作用域與上層一系列父級作用域組成,作用域的頭部永遠是當前作用域,尾部永遠是全域性作用域。作用域鏈保證了當前上下文對其有權訪問的變數的有序訪問。

我們先簡單瞭解下,詳細的我們會在徹底搞懂JavaScript作用域中談到。

var a = 1;
function foo() {
    function baz() {
        console.log( a ); 
    }

    baz();
}

foo(); // 1
複製程式碼

上面的對於我們來說很簡單,是吧?沒錯這就是作用域鏈的應用。

我們簡單模擬下

EC(foo) = {
    VO(foo): {...}, //省略
    ScopeChain: [VO(foo), window],
    this: 
}

EC(baz) = {
    VO(baz): {...}, //省略
    ScopeChain: [VO(baz), VO(foo), window],
    this: 
}
複製程式碼

3. 確定this指向

談到this,大家是不是感到很興奮,平時寫程式碼時,被這傢伙整的暈頭轉向的,這回我們終於可以揭開this的神祕面紗了,搞清楚它在JS到底是怎樣的存在,不過客官彆著急,我們這裡先不介紹this,因為關於this的內容太多了,我們得慢慢去品味它,這裡先記住,this是在執行上下文建立階段確定的

this傳送門???

this真是一個淘氣鬼

全域性上下文

全域性上下文有些特殊,其變數物件永遠是window,this永遠指向window(在瀏覽器中,Node中不是)。

EC(global) = {
    VO: window,
    ScopeChain: {},
    this: window
}
複製程式碼

執行階段

在執行階段變數物件(Variable Object)變為活動物件(Active Object)。 VO => AO

這樣,如果再面試的時候被問到變數物件和活動物件有什麼區別,就又可以自如的應答了,他們其實都是同一個物件,只是處於執行上下文的不同生命週期。不過只有處於函式呼叫棧棧頂的執行上下文中的變數物件,才會變成活動物件。

執行階段JS引擎會進行變數賦值函式引用執行其他程式碼,執行順序取決於程式碼的位置。

我們就聊到這吧。

喝杯茶,

休息一下。

相關文章