1. 程式人生 > >執行上下文

執行上下文

src 同步 ring 放心 新的 ava execution 就會 上下

上下文的原意是 context, 作用域的原意是scope, 這兩個不是一個東西。

每一個函數的調用(function invocation) 都有對應的scopecontext.

scope 指的是 函數被調用的時候, 各個變量的作用區域
context 指的是 current scope and its enclosing scope. 就是當前scope 和包裹它外面的scope. 如果一個變量在當前scope沒找到,那麽它會自底向上繼續找enclosing scope 直到找到為止很像javascript 的prototype那樣的找法。經常在javascript中,函數被調用的時候, 查看this

指向哪個object, 那麽那個object 就是當前的 "上下文"。

小明告訴小紅:“你放心吧,他答應你的條件了。”

在讀者的眼中,“他”是誰根本無從知曉,因為這句話缺少“上下文”;

從小強家裏出來後,小明告訴小紅:“你放心吧,他答應你的條件了。”

誰都知道,“他”指的是“小強”,因為有“上下文”。

什麽是執行上下文

Javascript中代碼的運行環境分為以下三種:

  • 全局級別的代碼 – 這個是默認的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境。
  • 函數級別的代碼 – 當執行一個函數時,運行函數體中的代碼。
  • Eval的代碼 – 在Eval函數內運行的代碼。

在網上可以找到很多闡述作用域的資源,為了使該文便於大家理解,我們可以將“執行上下文”看做當前代碼的運行環境或者作用域。下面我們來看一個示例,其中包括了全局以及函數級別的執行上下文:

技術分享圖片

上圖中,一共用4個執行上下文。紫色的代表全局的上下文;綠色代表person函數內的上下文;藍色以及橙色代表person函數內的另外兩個函數的上下文。註意,不管什麽情況下,只存在一個全局的上下文,該上下文能被任何其它的上下文所訪問到。也就是說,我們可以在person的上下文中訪問到全局上下文中的sayHello變量,當然在函數firstName或者lastName中同樣可以訪問到該變量。

至於函數上下文的個數是沒有任何限制的,每到調用執行一個函數時,引擎就會自動新建出一個函數上下文,換句話說,就是新建一個局部作用域,可以在該局部作用域中聲明私有變量等,在外部的上下文中是無法直接訪問到該局部作用域內的元素的

。在上述例子的,內部的函數可以訪問到外部上下文中的聲明的變量,反之則行不通。那麽,這到底是什麽原因呢?引擎內部是如何處理的呢?

執行上下文堆棧

在瀏覽器中,javascript引擎的工作方式是單線程的。也就是說,某一時刻只有唯一的一個事件是被激活處理的,其它的事件被放入隊列中,等待被處理。下面的示例圖描述了這樣的一個堆棧:

技術分享圖片

我們已經知道,當javascript代碼文件被瀏覽器載入後,默認最先進入的是一個全局的執行上下文。當在全局上下文中調用執行一個函數時,程序流就進入該被調用函數內,此時引擎就會為該函數創建一個新的執行上下文,並且將其壓入到執行上下文堆棧的頂部。瀏覽器總是執行當前在堆棧頂部的上下文,一旦執行完畢,該上下文就會從堆棧頂部被彈出,然後,進入其下的上下文執行代碼。這樣,堆棧中的上下文就會被依次執行並且彈出堆棧,直到回到全局的上下文。請看下面一個例子:

 (function foo(i) {
            if (i === 3) {
                return;
            }
            else {
                foo(++i);
            }
        }(0));

上述foo被聲明後,通過()運算符強制直接運行了。函數代碼就是調用了其自身3次,每次是局部變量i增加1。每次foo函數被自身調用時,就會有一個新的執行上下文被創建。每當一個上下文執行完畢,該上上下文就被彈出堆棧,回到上一個上下文,直到再次回到全局上下文。真個過程抽象如下圖:

技術分享圖片

由此可見 ,對於執行上下文這個抽象的概念,可以歸納為以下幾點:

  • 單線程
  • 同步執行
  • 唯一的一個全局上下文
  • 函數的執行上下文的個數沒有限制
  • 每次某個函數被調用,就會有個新的執行上下文為其創建,即使是調用的自身函數,也是如此。

執行上下文的建立過程

我們現在已經知道,每當調用一個函數時,一個新的執行上下文就會被創建出來。然而,在javascript引擎內部,這個上下文的創建過程具體分為兩個階段:

  1. 建立階段(發生在當調用一個函數時,但是在執行函數體內的具體代碼以前)
    • 建立變量,函數,arguments對象,參數
    • 建立作用域鏈
    • 確定this的值
  2. 代碼執行階段:
    • 變量賦值,函數引用,執行其它代碼

實際上,可以把執行上下文看做一個對象,其下包含了以上3個屬性:

    
          (executionContextObj = {
            variableObject: { /* 函數中的arguments對象, 參數, 內部的變量以及函數聲明 */ },
            scopeChain: { /* variableObject 以及所有父執行上下文中的variableObject */ },
            this: {}
          }
    

建立階段以及代碼執行階段的詳細分析

確切地說,執行上下文對象(上述的executionContextObj)是在函數被調用時,但是在函數體被真正執行以前所創建的。函數被調用時,就是我上述所描述的兩個階段中的第一個階段 – 建立階段。這個時刻,引擎會檢查函數中的參數,聲明的變量以及內部函數,然後基於這些信息建立執行上下文對象(executionContextObj)。在這個階段,variableObject對象,作用域鏈,以及this所指向的對象都會被確定。

上述第一個階段的具體過程如下:

  1. 找到當前上下文中的調用函數的代碼
  2. 在執行被調用的函數體中的代碼以前,開始創建執行上下文
  3. 進入第一個階段-建立階段:

    • 建立variableObject對象:
      1. 建立arguments對象,檢查當前上下文中的參數,建立該對象下的屬性以及屬性值
      2. 檢查當前上下文中的函數聲明:

        每找到一個函數聲明,就在variableObject下面用函數名建立一個屬性,屬性值就是指向該函數在內存中的地址的一個引用

        如果上述函數名已經存在於variableObject下,那麽對應的屬性值會被新的引用所覆蓋。

    • 初始化作用域鏈
    • 確定上下文中this的指向對象
  4. 代碼執行階段:

    執行函數體中的代碼,一行一行地運行代碼,給variableObject中的變量屬性賦值。

下面來看個具體的代碼示例:

    
        function foo(i) {
            var a = ‘hello‘;
            var b = function privateB() {
        
            };
            function c() {
        
            }
        }
        
        foo(22);
    

在調用foo(22)的時候,建立階段如下:

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

由此可見,在建立階段,除了arguments,函數的聲明,以及參數被賦予了具體的屬性值,其它的變量屬性默認的都是undefined。一旦上述建立階段結束,引擎就會進入代碼執行階段,這個階段完成後,上述執行上下文對象如下:

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

我們看到,只有在代碼執行階段,變量屬性才會被賦予具體的值。

局部變量作用域提升的緣由

在網上一直看到這樣的總結: 在函數中聲明的變量以及函數,其作用域提升到函數頂部,換句話說,就是一進入函數體,就可以訪問到其中聲明的變量以及函數。這是對的,但是知道其中的緣由嗎?相信你通過上述的解釋應該也有所明白了。不過在這邊再分析一下。看下面一段代碼:

    
        (function() {
            console.log(typeof foo); // function pointer
            console.log(typeof bar); // undefined
        
            var foo = ‘hello‘,
                bar = function() {
                    return ‘world‘;
                };
        
            function foo() {
                return ‘hello‘;
            }
        
        }());?
    

上述代碼定義了一個匿名函數,並且通過()運算符強制理解執行。那麽我們知道這個時候就會有個執行上下文被創建,我們看到例子中馬上可以訪問foo以及bar變量,並且通過typeof輸出foo為一個函數引用,bar為undefined。

為什麽我們可以在聲明foo變量以前就可以訪問到foo呢?

因為在上下文的建立階段,先是處理arguments, 參數,接著是函數的聲明,最後是變量的聲明。那麽,發現foo函數的聲明後,就會在variableObject下面建立一個foo屬性,其值是一個指向函數的引用。當處理變量聲明的時候,發現有var foo的聲明,但是variableObject已經具有了foo屬性,所以直接跳過。當進入代碼執行階段的時候,就可以通過訪問到foo屬性了,因為它已經就存在,並且是一個函數引用。

為什麽bar是undefined呢?

因為bar是變量的聲明,在建立階段的時候,被賦予的默認的值為undefined。由於它只要在代碼執行階段才會被賦予具體的值,所以,當調用typeof(bar)的時候輸出的值為undefined。

這樣差不多對執行上下文有所理解了吧,大佬們希望能給出更好地建議

執行上下文