執行上下文

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

每個函數的調用(function invocation) 都有對應的scope 和context.java

scope 指的是 函數被調用的時候, 各個變量的做用區域
context 指的是 current scope and its enclosing scope. 就是當前scope 和包裹它外面的scope. 若是一個變量在當前scope沒找到,那麼它會自底向上繼續找enclosing scope 直到找到爲止很像javascript 的prototype那樣的找法。常常在javascript中,函數被調用的時候, 查看this 指向哪一個object, 那麼那個object 就是當前的 "上下文"瀏覽器

 

小明告訴小紅:「你放心吧,他答應你的條件了。」函數

  在讀者的眼中,「他」是誰根本無從知曉,由於這句話缺乏「上下文」;this

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

   誰都知道,「他」指的是「小強」,由於有「上下文」。prototype

 

什麼是執行上下文

Javascript中代碼的運行環境分爲如下三種:線程

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

在網上能夠找到不少闡述做用域的資源,爲了使該文便於你們理解,咱們能夠將「執行上下文」看作當前代碼的運行環境或者做用域。下面咱們來看一個示例,其中包括了全局以及函數級別的執行上下文:3d

上圖中,一共用4個執行上下文。紫色的表明全局的上下文;綠色表明person函數內的上下文;藍色以及橙色表明person函數內的另外兩個函數的上下文。注意,無論什麼狀況下,只存在一個全局的上下文,該上下文能被任何其它的上下文所訪問到。也就是說,咱們能夠在person的上下文中訪問到全局上下文中的sayHello變量,固然在函數firstName或者lastName中一樣能夠訪問到該變量。code

至於函數上下文的個數是沒有任何限制的,每到調用執行一個函數時,引擎就會自動新建出一個函數上下文,換句話說,就是新建一個局部做用域,能夠在該局部做用域中聲明私有變量等,在外部的上下文中是沒法直接訪問到該局部做用域內的元素的。在上述例子的,內部的函數能夠訪問到外部上下文中的聲明的變量,反之則行不通。那麼,這究竟是什麼緣由呢?引擎內部是如何處理的呢?

執行上下文堆棧

在瀏覽器中,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。

這樣差很少對執行上下文有所理解了吧,大佬們但願能給出更好地建議

相關文章
相關標籤/搜索