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

在這篇文章中,將深刻研究JavaScript最基本的部分之一,即執行上下文。在這篇文章的最後,你應該更清楚地理解解釋器要作什麼,爲何在聲明一些函數/變量以前可使用它們,以及它們的值是如何肯定的。web

什麼是執行上下文

當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個不一樣的函數上下文,全局上下文能夠被程序中的其它任何上下文訪問。ui

你能夠有任意數量的函數上下文,每一個函數被調用的時候都會建立一個新的上下文。每一個下文都有一個不能被外部函數直接訪問到的內部變量的私有做用域。在上面代碼的例子中,一個函數能夠訪問當前上下文外部聲明的變量,可是一個外部上下文不能夠訪問函數內部聲明的變量。this

執行上下文堆棧

瀏覽器中的JavaScript解釋器是做爲一個單線程實現的,這實際上意味着,在瀏覽器中,一次只能發生一件事,其餘操做或事件將排隊在所謂的執行堆棧中。spa

當瀏覽器開始執行腳本時,首先會默認進入全局執行上下文,若是在全局代碼中調用了函數,程序會按照順序進入被調用函數,建立一個新的執行上下文,並推入到執行棧的棧頂。線程

若是你在當前執行的函數中,調用了另外的函數,代碼的執行流將會進入函數內部,並建立一個新的執行上下文推入到執行棧頂。瀏覽器老是會先執行棧頂的代碼,而且一旦函數完成執行當前執行上下文,他就會從棧頂彈出,將控制權返回到當前堆棧中的上下文。指針

關於執行堆棧有如下關鍵點code

  • 單線程
  • 同步執行
  • 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解釋器是如何執行代碼的。理解執行上下文和堆棧可讓你瞭解代碼沒有按照預期執行的緣由

相關文章
相關標籤/搜索