Javascript中你必須理解的執行上下文和調用棧

執行上下文在 JavaScript 是很是重要的基礎知識,想要理解 JavaScript 的執行過程,執行上下文 是你必需要掌握的知識。不然只能是知其然不知其因此然。javascript

理解執行上下文有什麼好處呢?java

它能夠幫助你更好的理解代碼的執行過程,做用域,閉包等關鍵知識點。特別是閉包它是 JavaScript 中的一個難點,當你理解了執行上下文在回頭看閉包時,應該會有豁然開朗的感受。git

這篇文章咱們將深刻了解 執行上下文,讀完文章以後你應該能夠清楚的瞭解到 JavaScript 解釋器到底作了什麼,爲何能夠在一些函數和變量以前使用它,以及它們的值是如何肯定的。github

什麼是執行上下文

在 JavaScript 中運行代碼時,代碼的執行環境很是重要,一般是下列三種狀況:瀏覽器

  • Global code:代碼第一次執行時的默認環境。
  • Function code:函數體中的代碼
  • Eval code:eval 函數內執行的文本(實際開發中不多使用,因此見到的狀況很少)

在網上你能夠讀到不少關於做用域的文章,爲了便於理解本文的內容,咱們將 執行上下文 看成代碼的 執行環境/做用域。如今就讓咱們看一個例子:它包括 全局和函數/本地執行上下文。閉包

上面的例子咱們看到,紫色的框表明全局上下文,綠色、藍色、橙色表明三個不一樣的函數上下文。全局上下文執行有一個,它能夠被其餘上下文訪問到。函數

你能夠有任意數量的函數上下文,每一個函數在調用時都會建立一個新的上下文,它是一個私有範圍,函數內部聲明的全部東西都不能在函數做用域外訪問到。ui

上面的例子中,函數內部能夠訪問當前上下文以外聲明的變量,可是外部卻不能訪問函數內部的變量/函數。這究竟是爲何?其中的代碼是如何執行的?this

執行上下文棧

瀏覽器中的 JavaScript 解釋器是單線程實現的。這意味着在瀏覽器中一次只能作一件事情。而其餘的行爲或事件都會在執行棧中排隊等待。如圖:spa

咱們知道,當瀏覽器第一次加載腳本時,默認狀況下,它會進入全局上下文。若是在全局代碼中調用了一個函數,則代碼的執行會進入函數中,此時會建立一個新的執行上下文,它會被推到執行上下文棧中。

若是在這個過程當中函數內部調用了另外一個函數,會發生一樣的事情,代碼的執行會進入函數中,而後建立一個新的執行上下文,它會被推到上下文棧 的頂部。瀏覽器始終執行棧頂部的執行上下文。

一旦函數完成執行,當前的執行上下文將從棧的頂部彈出,而後繼續執行下面的,下面程序演示了一個遞歸函數的執行上下文狀況。

(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));
複製代碼

本身調用本身三次,每次將 i 遞增 1,每次函數 foo 被調用的時候,就會建立一個新的執行上下文。一旦當前上下文執行完畢以後,它就會從棧中彈出並轉移到下面的上下文中,直到全局上下。

執行上下文棧的 5 個關鍵點:

  • 單線程
  • 同步執行
  • 只有一個全局上下文
  • 任意數量的函數上下文
  • 每一個函數調用都會建立一個新的執行上下文,包括本身調用本身

詳解執行上下文

到此,咱們知道每次調用一個函數時,都會建立一個新的執行上下文。可是在 JavaScript 解釋器中,每次調用執行上下文會有兩個階段:

  1. 建立階段
    • 建立做用域鏈
    • 建立變量,函數,arguments列表。
    • 肯定 this 的指向
  2. 執行階段
    • 賦值,尋找函數引用,解釋/執行代碼

執行上下文能夠抽象爲一個對象它具有三個屬性:

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

活動/變量對象[AO/VO]

executionContextObj 對象在函數調用時建立,但它是在函數真正執行以前就建立的,這就是咱們所說的第一個階段 建立階段,此時解釋器經過掃描函數的傳入參數,arguments,本地函數聲明,局部變量聲明來建立executionContextObj 對象。將結果變成 variableObject 放入 executionContextObj 中。

解釋器執行代碼時的大體描述:

  1. 調用函數
  2. 在執行代碼時,建立執行上下文
  3. 進入建立階段
    • 初始化做用域鏈
    • 建立變量對象(variableObject)
    • 建立參數對象(arguments object),檢查參數的上下文,初始化名稱和值,並建立引用副本
    • 掃描上下文中的函數聲明
    • 每發現一個函數,就會在 variableObject 中建立一個名稱,保存函數的引用
    • 若是名稱已經存在,則覆蓋引用
    • 掃描上下文中的變量聲明
    • 每發現一個變量,就在 variableObject 中建立一個名稱,並初始化值爲 undefined
    • 若是變量名已經存在,什麼都不作,繼續掃描
    • 肯定上下文中的 this 指向
  4. 執行代碼階段
    • 在上下文中執行/解釋代碼,在代碼逐行執行時進行變量復賦值

讓咱們看一個例子:

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: { ... }
}
複製代碼

如上所述,除了形參 i 和 arguments外,在建立階段咱們只把變量進行聲明而不進行賦值。

在建立階段完成後,程序會進入函數中執行代碼,以下所示:

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

聲明提早

網上不少關於聲明提早的內容,它是用來解釋變量和函數在聲明時會被提早到做用域的頂部。可是並無人詳細解釋爲何會發生這種狀況,有了剛纔關於解釋器如何建立活動對象(AO)的認知,咱們將很容易看出緣由。例如:

(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 顯示的是 function 而不是 undefined 或者 string

雖然 foo 被聲明瞭兩次,可是咱們在建立階段中說到,函數是在變量以前建立在變量對象中,當變量對象中名稱已經存在時,變量聲明什麼也不作。

所以 foo 會被先建立爲函數 function foo() 的引用,當執行到 var foo時發現變量對象中已將存在了,因此此時什麼也不作,而是繼續掃描。

爲何 bar 是 undefined

bar 其實是一個變量只不過它的值是函數,而變量在建立階段的值爲 undefined

總結

咱們再來梳理下重要的知識點:

  1. 首先在程序執行時會建立一個全局的執行上下文,有且只有一個。
  2. 函數在每次調用時就會建立一個函數上下文,能夠有不少。
  3. 函數上下文能夠訪問全局上下文的內容,反之則不行。
  4. 建立的上下文會被推入到上下文棧中,而後從頂部開始依次執行。
  5. 執行上下文會分爲兩個階段:建立階段和執行階段。
  6. 建立階段會先進行函數聲明和變量聲明提早。
  7. 建立階段會先進行函數聲明,而後進行變量聲明,同時會被放入變量對象中,若是變量對象中已經存在:函數則進行引用的覆蓋,變量則什麼都不作。
  8. 執行階段纔會進行賦值和運行。

但願你已經理解了 JavaScript 解釋器是如何執行你的代碼的。理解執行上下文和 執行上下文棧可以讓你清楚的知道你的代碼爲何和預期的值不同。

你認爲了解,解釋器的內部原理是多餘仍是必須的知識?它是否可以幫助你更好的編寫 JavaScript 代碼?歡迎留言討論。

原文:davidshariff.com/blog/what-i…
翻譯:六小登登
更多優質文章:六小登登的博客

我是:六小登登,一名愛寫做的技術人。 關注公衆號:六小登登,後臺回覆「1024」便可免費獲取驚喜福利!後臺回覆「加羣」羣裏天天都會全網蒐羅好文章給你。

相關文章
相關標籤/搜索