js - 基礎 之 預編譯總結

js運行步驟

  • 語法解析(檢查有無語法錯誤)
  • 預編譯
  • 解釋運行(將 js 翻譯成計算機識別的語言(0、1組成),翻譯一行執行一行)

預編譯

【全局】:javascript

  • 建立 GO( Grobal Object ) 對象
  • 找變量聲明
  • 找函數聲明

【函數】:html

  • 建立 AO( Activation Object ) 對象(執行上下文);
  • 找形參和變量聲明,將形參和變量名做爲 AO 對象的屬性名,值爲 undefined(有重複的名稱只寫一個便可);
  • 將形參與實參值統一(用實參的值替換 undefined);
  • 在函數體中找函數聲明,將函數名添加到 AO 對象的屬性中,值爲函數體(如屬性名重複,則覆蓋前面的)。

【執行上下文/執行環境】組成:前端

  • 變量對象(Variable object,VO) :包含變量的對象,沒法訪問。
  • 做用域鏈(Scope chain):做用域即變量對象,做用域鏈是一個由變量對象組成的帶頭結點的單向鏈表,其主要做用就是用來進行變量查找;而[[Scope]]屬性是一個指向這個鏈表頭節點的指針
  • this:指向一個環境對象

【參考】https://www.jianshu.com/p/76ed896bbf91java

執行環境(執行上下文 execution context)

定義

有時也稱環境,執行環境定義了變量或函數有權訪問的其餘數據 ,決定了它們各自的行爲。而每一個執行環境都有一個與之相關的變量對象,環境中定義的全部變量和函數都保存在這個對象中。git

執行過程

當JavaScript解釋器初始化執行代碼時,它首先默認進入全局執行環境,今後刻開始,函數的每次調用都會建立一個新的執行環境。
當javascript代碼被瀏覽器載入後,默認最早進入的是一個全局執行環境。
當在全局執行環境中調用執行一個函數時,程序流就進入該被調用函數內,此時JS引擎就會爲該函數建立一個新的執行環境,而且將其壓入到執行環境堆棧的頂部。瀏覽器老是執行當前在堆棧頂部的執行環境,一旦執行完畢,該執行環境就會從堆棧頂部被彈出,而後,進入其下的執行環境執行代碼。這樣,堆棧中的執行環境就會被依次執行而且彈出堆棧,直到回到全局執行環境。github

執行環境完成能夠分爲建立執行兩個階段

一、在建立階段,解析器首先會建立一個`變量對象`【variable object】(函數中稱爲`活動對象`【activation object】),它由定義在執行環境中的變量、函數聲明、和參數組成。在這個階段,做用域鏈會被初始化,this的值也會被最終肯定。
二、在執行階段,代碼被解釋執行。 
具體過程:每次調用函數,都會建立新的執行上下文。在JavaScript解釋器內部,每次調用執行上下文,分爲兩個階段:
   2.1 建立階段【如果函數,當函數被調用,但未執行任何其內部代碼以前】
       在進入執行上下文階段,只會將有 var,function修飾的變量或方法添加到變量對象中。
          2.1.1 建立做用域鏈(Scope Chain)
          2.1.2 建立變量對象(變量,函數和參數)
          2.1.3 肯定this的指向  
   2.2 激活/代碼執行階段:
         2.2.1 變量賦值
         2.2.2 函數引用,
         2.2.3  解釋/執行其餘代碼

變量對象組成

  • 函數的全部形參 (若是是函數上下文)
    由名稱和對應值組成的一個變量對象的屬性被建立
    沒有實參,屬性值設爲 undefined數組

  • 函數聲明
    由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被建立
    若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性瀏覽器

  • 變量聲明
    由名稱和對應值(undefined)組成一個變量對象的屬性被建立;
    若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性閉包

能夠將每一個執行上下文抽象爲一個對象並有三個屬性:函數

executionContextObj = {
  scopeChain: { /* 變量對象(variableObject)+ 全部父執行上下文的變量對象*/ }, 
  variableObject: { /*函數 arguments/參數,內部變量和函數聲明 */ }, 
  this: {} 
}

解釋器執行代碼的僞邏輯(函數)

一、查找調用函數的代碼。
二、執行函數代碼以前,先建立執行上下文。
三、進入建立階段:
  3.1 初始化做用域鏈:
  3.2 建立變量對象:
   3.2.1 建立arguments對象,檢查上下文,初始化參數名稱和值並建立引用的複製。
   3.2.2 掃描上下文的函數聲明:
     爲發現的每個函數,在變量對象上建立一個屬性——確切的說是函數的名字——其有一個指向函數在內存中的引用。
     若是函數的名字已經存在,引用指針將被重寫。
   3.2.3 掃描上下文的變量聲明:
     爲發現的每一個變量聲明,在變量對象上建立一個屬性——就是變量的名字,而且將變量的值初始化爲undefined
     若是變量的名字已經在變量對象裏存在,將不會進行任何操做並繼續掃描。   
  3.3 求出上下文內部「this」的值。
四、激活/代碼執行階段:
在當前上下文上運行/解釋函數代碼,並隨着代碼一行行執行指派變量的值。

demo:

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

一、建立階段:foo(22)函數調用時

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

二、執行階段:執行流進入函數而且激活/代碼執行階段

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

注意:

  1. 單線程
  2. 同步執行
  3. 惟一的全局執行環境
  4. 局部執行環境的個數沒有限制
  5. 每次某個函數被調用,就會有個新的局部執行環境爲其建立,即便是屢次調用的自身函數(即一個函數被調用屢次,也會建立多個不一樣的局部執行環境)。

執行環境的分類

  1. Global Code,即全局的、不在任何函數裏面的代碼,例如:一個js文件、嵌入在HTML頁面中的js代碼等。
  2. Function Code,即用戶自定義函數中的函數體JS代碼。
  3. Eval Code,即便用eval()函數動態執行的JS代碼。【不推薦,可忽略】

    全局環境:JavaScript代碼運行起來會首先進入該環境
    函數環境:當函數被調用執行時,會進入當前函數中執行代碼
    eval(不建議使用,可忽略)

解釋:

  1. 全局執行環境
    在瀏覽器中,其指window對象,是JS代碼開始運行時的默認環境。
    全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。
  2. 函數執行環境
    當某個函數被調用時,會先建立一個執行環境及相應的做用域鏈。而後使用arguments和其餘命名參數的值來初始化執行環境的變量對象。

執行上下文(execution context)屬性:

  • 變量對象(variableObject)
  • 做用域鏈(scope chain)
  • this

【注】:
變量對象是與執行上下文相關的數據做用域,存儲了在上下文中定義的變量和函數聲明。
全局上下文中的變量對象就是全局對象
在函數上下文中,咱們用活動對象(activation object, AO)來表示變量對象。


AO & VO 區別與聯繫
活動對象和變量對象實際上是一個東西,只是變量對象是規範上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象纔會被激活,因此才叫 activation object 吶,而只有被激活的變量對象,也就是活動對象上的各類屬性才能被訪問。

活動對象是在進入函數上下文時刻被建立的,它經過函數的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。

AO = VO + function parameters + arguments
AO 還包含函數的 parameters,以及 arguments 這個特殊對象

未進入執行階段以前,變量對象(VO)中的屬性都不能訪問!可是進入執行階段以後,變量對象(VO)被激活轉變爲了活動對象(AO),裏面的屬性都能被訪問了,而後開始進行執行階段的操做。
它們其實都是同一個對象,只是處於執行上下文的不一樣生命週期


做用域鏈建立

詞法做用域(lexical scoping)是指,函數在執行時,使用的是它被定義時的做用域,而不是這個函數被調用時的做用域
函數的做用域在函數定義的時候就決定了。
這是由於函數有一個內部屬性 [[scope]],當函數建立的時候,就會保存全部父變量對象到其中,你能夠理解 [[scope]] 就是全部父變量對象的層級鏈,可是注意:[[scope]] 並不表明完整的做用域鏈!
當函數激活時,進入函數上下文,建立 VO/AO 後,就會將活動對象添加到做用鏈的前端。至此,做用域鏈建立完畢。

demo:

function foo() {
            function bar() {
                ...
            }
        }
        // 函數建立時,各自的[[scope]]爲:

        foo.[[scope]] = [
          globalContext.VO
        ];

        bar.[[scope]] = [
            fooContext.AO,
            globalContext.VO
        ];

函數執行上下文中做用域鏈和變量對象的建立過程總結

var scope = "global scope";
        function checkscope(){
            var scope2 = 'local scope';
            return scope2;
        }
        checkscope();

執行過程以下:

// 1.checkscope 函數被建立,保存做用域鏈到 內部屬性[[scope]]
        checkscope.[[scope]] = [
            globalContext.VO
        ];

        // 2.執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
        ECStack = [
            checkscopeContext,
            globalContext
        ];

        // 3.checkscope 函數並不馬上執行,開始作準備工做,第一步:複製函數[[scope]]屬性建立做用域鏈
        checkscopeContext = {
            Scope: checkscope.[[scope]],
        }

        // 4.第二步:用 arguments 建立活動對象,隨後初始化活動對象,加入形參、函數聲明、變量聲明
        checkscopeContext = {
            AO: {
                arguments: {
                    length: 0
                },
                scope2: undefined
            },
            Scope: checkscope.[[scope]],
        }

        // 5.第三步:將活動對象壓入 checkscope 做用域鏈頂端
        checkscopeContext = {
            AO: {
                arguments: {
                    length: 0
                },
                scope2: undefined
            },
            Scope: [AO, [[Scope]]]
        }

        // 6.準備工做作完,開始執行函數,隨着函數的執行,修改 AO 的屬性值
        checkscopeContext = {
            AO: {
                arguments: {
                    length: 0
                },
                scope2: 'local scope'
            },
            Scope: [AO, [[Scope]]]
        }

        // 7.查找到 scope2 的值,返回後函數執行完畢,函數上下文從執行上下文棧中彈出
        ECStack = [
            globalContext
        ];

閉包(能夠用執行上下文中來解釋)

  • 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
  • 在代碼中引用了自由變量

內部函數引用了外部函數的變量,在外部函數上下文被銷燬後,其中的變量仍然能夠被其內部函數引用
由於:

fContext = {
  Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

對的,就是由於這個做用域鏈,f 函數依然能夠讀取到 checkscopeContext.AO 的值,說明當 f 函數引用了 checkscopeContext.AO 中的值的時候,即便 checkscopeContext 被銷燬了,可是 JavaScript 依然會讓 checkscopeContext.AO 活在內存中,f 函數依然能夠經過 f 函數的做用域鏈找到它,正是由於 JavaScript 作到了這一點,從而實現了閉包這個概念。

說明:
我的總結,筆記,有誤請指出,謝謝指教~
ps: 文章格式暫時沒時間處理,以後優化【捂臉】

參考:
一、http://www.cnblogs.com/neusc/p/5771150.html
二、https://yanhaijing.com/javascript/2014/04/29/what-is-the-execution-context-in-javascript/
三、https://github.com/mqyqingfeng/Blog/issues/8
四、https://github.com/mqyqingfeng/Blog/issues/6

相關文章
相關標籤/搜索