JavaScript中的執行上下文和堆疊是什麼

lvwxx發表於2019-04-07

在這篇文章中,將深入研究JavaScript最基本的部分之一,即執行上下文。在這篇文章的最後,你應該更清楚地理解直譯器要做什麼,為什麼在宣告一些函式/變數之前可以使用它們,以及它們的值是如何確定的。

什麼是執行上下文

當JavaScript程式碼執行時,執行程式碼的環境是相當重要的。一般有以下三種情況:

  • 全域性程式碼 -- 程式碼首次開始執行的預設環境
  • 函式程式碼 -- 每當進入一個函式內部
  • Eval程式碼 -- eval內部程式碼執行時

把執行上下文看作是當前程式碼正在執行的環境/作用域

// global context
var sayHello = 'sayHello'

function person() {
  var first = 'webb'
  var last = 'wang'

  function firstName() {
    return first
  }

  function lastName() {
    return last
  }

  console.log(sayHello + firstName() + '' + lastName())
}
複製程式碼

以上程式碼沒什麼特別的地方,它包括1個全域性上下文和3個不同的函式上下文,全域性上下文可以被程式中的其它任何上下文訪問。

你可以有任意數量的函式上下文,每個函式被呼叫的時候都會建立一個新的上下文。每個下文都有一個不能被外部函式直接訪問到的內部變數的私有作用域。在上面程式碼的例子中,一個函式可以訪問當前上下文外部宣告的變數,但是一個外部上下文不可以訪問函式內部宣告的變數。

執行上下文堆疊

瀏覽器中的JavaScript直譯器是作為一個單執行緒實現的,這實際上意味著,在瀏覽器中,一次只能發生一件事,其他操作或事件將排隊在所謂的執行堆疊中。

當瀏覽器開始執行指令碼時,首先會預設進入全域性執行上下文,如果在全域性程式碼中呼叫了函式,程式會按照順序進入被呼叫函式,建立一個新的執行上下文,並推入到執行棧的棧頂。

如果你在當前執行的函式中,呼叫了另外的函式,程式碼的執行流將會進入函式內部,並建立一個新的執行上下文推入到執行棧頂。瀏覽器總是會先執行棧頂的程式碼,並且一旦函式完成執行當前執行上下文,他就會從棧頂彈出,將控制權返回到當前堆疊中的上下文。

關於執行堆疊有以下關鍵點

  • 單執行緒
  • 同步執行
  • 1個全域性上下文
  • 每個函式呼叫都會建立一個新的執行上下文,即使呼叫它自身。

深入理解執行上下文

現在我們知道每當有函式被呼叫時,都會建立一個新的執行上下文。在js內部,每個執行上文建立都要經歷下面2個階段

1.建立階段(函式被呼叫,但還沒有執行內部程式碼)

  • 建立作用域鏈
  • 建立變數和引數
  • 決定this指向

2.程式碼執行階段

  • 變數賦值,執行程式碼

可以將每個執行上下文概念上表示為一個具有3個屬性的物件:

executionContextObj = {
  'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
  'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
  'this': {}
}
複製程式碼

活動物件/變數物件(AO/VO)

當函式被呼叫時,在建立階段直譯器會建立包含有函式內部變數,引數的一個變數物件

下面是直譯器如何評估程式碼的概述

  1. 掃描被呼叫函式中的程式碼
  2. 在程式碼執行前,建立執行上文
  3. 進入建立階段
    • 初始化作用域鏈
    • 建立變數物件
    • 建立arguments物件,檢查引數上下文,初始化名稱和值,並建立引用副本
    • 掃描上下文中函式的宣告
      • 對於找到的每個函式,在變數物件中建立一個屬性,該屬性是確切的函式名,該函式在記憶體中有一個指向該函式的引用指標
      • 如果函式名已經存在,指標將會被覆蓋
    • 掃描變數的宣告
      • 對於找到的每個變數,在變數物件中建立一個屬性,該屬性是確切的變數名,該變數的值是undefined
      • 如果變數名已經存在,將不會做任何處理繼續執行
    • 決定this的值
  4. 程式碼執行階段
    • 變數賦值,按順序執行程式碼

宣告提升

你可以在網上找到許多用JavaScript定義術語提升的資源,解釋變數和函式宣告被提升到函式作用域的頂部。但是,沒有人詳細解釋為什麼會發生這種情況,而且有了直譯器如何建立啟用物件的新知識,就很容易理解為什麼會發生這種情況。以下面的程式碼為例:

(function() {
  console.log(typeof foo); // function pointer
  console.log(typeof bar); // undefined

  var foo = 'hello',
      bar = function() {
          return 'world';
      };

  function foo() {
      return 'hello';
  }

}());​
複製程式碼

為什麼在什麼之前可以訪問到foo

如果我們遵循建立階段,我們就知道在程式碼執行階段之前已經建立了變數。因此,當函式流開始執行時,foo已經在活動物件中定義。

Foo宣告瞭兩次,為什麼Foo是函式而不是未定義或字串?

儘管foo宣告瞭兩次,但從建立階段我們就知道函式是在變數之前在變數物件上建立的,如果變數物件上的屬性名已經存在,那麼我們只需繞過。 因此,首先在變數物件上建立對函式foo()的引用,當直譯器到達var foo時,我們已經看到了屬性名foo的存在,所以程式碼什麼也不做,繼續執行

為什麼bar是undefined

bar實際上是一個具有函式賦值的變數,我們知道這些變數是在建立階段建立的,但是它們是用undefined值初始化的。

總結

希望現在你已經很好地理解了JavaScript直譯器是如何執行程式碼的。理解執行上下文和堆疊可以讓你瞭解程式碼沒有按照預期執行的原因

相關文章