# JavaScript中的執行上下文和隊列(棧)的關係?

  • 原文:What is the Execution Context & Stack in JavaScript?
  • git地址:JavaScript中的執行上下文和隊列(棧)的關係?
  • 導讀:之前老是看到相關文章提到什麼變量提高,函數提高啥的,什麼函數提高優先級大於變量的,老是知其然,不知其因此然,當面試官拿着同一name,卻不斷function, 和var賦值,而後讓你告訴他每個階段該是什麼值的時候,拿着啥變量提高和函數提高是解釋不通的,至少我不能-_-。David Shariff的這篇文章爲咱們講述了其中的原理,讓人看了豁然開朗
在這篇文章中,我將深刻探討JavaScript的一個最基本的部分,執行上下文。 在本文結束時,您會更清楚解釋器都作了些什麼,以致於某些函數、變量在聲明它們以前就可使用,它們的值是如何肯定的。

什麼是執行上下文?

當代碼在JavaScript中運行時,它的執行環境很是重要,而且它們分爲如下幾類:javascript

  • global 代碼 -- 首次執行代碼的默認環境
  • function 代碼 -- 每當執行流程進入函數體時
  • Eval 代碼 -- 要在內部eval 函數內執行的文本

爲了便於理解,本文中執行上下文是指:當前被執行的代碼的環境、做用域;接下來讓咱們看一個執行上下文中包含global、function content的代碼:java

圖片描述

這裏沒有什麼特別之處,1個global context由紫色邊框表示,3個不一樣的function contexts分別由綠色、藍色和橙色邊框表示。只能有1個global context,能夠從程序中的任何其餘上下文訪問。git

您能夠擁有任意數量的function contexts,而且每一個函數調用都會建立一個新的上下文,從而建立一個私有做用域,在該做用域內,沒法從當前函數做用域外直接訪問函數內部聲明的任何內容。在上面的示例中,函數能夠訪問在其當前上下文以外聲明的變量,但外部上下文沒法訪問在其內部聲明的變量/函數。爲何會這樣?這段代碼到底是如何運行的?github

執行上下文堆棧

瀏覽器中的JavaScript解釋器單線程運行。這就意味着同一時間瀏覽器只執行一件事,其它的事件在執行隊列中排隊。下圖是單線程隊列的抽象視圖:面試

圖片描述

咱們已經知道,當瀏覽器首次加載您的腳本時,它默認進入全局執行上下文(global execution contenrt)。若是在您的全局代碼中調用一個函數,程序的順序流進入被調用的函數,建立一個新函數execution context並將該上下文推送到頂部execution stack(執行隊列)。瀏覽器

若是在當前函數中調用另外一個函數,則會發生一樣的事情。代碼的執行流程進入內部函數,該函數建立一個execution context並推送到執行隊列的頂部。瀏覽器始終執行位於堆棧頂部的execution context,而且一旦函數完成執行當前操做execution context,它將從堆棧頂部彈出,將控制權返回到當前堆棧中的下方上下文。下面的例子顯示了一個遞歸函數和程序execution stack:閉包

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

代碼只調用自身3次,將i的值遞增1.每次調用foo函數時,都會建立一個新的執行上下文。一旦執行完成,它就會彈出堆棧而且將控制權交給它下面的上下文,直到再次到達global context(koa2的洋蔥圖想到了沒?)koa

如下是執行隊列的5個關鍵點:

  • 單線程、
  • 同步執行
  • 全局上下文
  • 無限級的函數上下文
  • 每一個函數調用都會建立一個新的執行上下文(execution context),包括對自身的調用(遞歸)

![es1](./static/es1.gif)

執行上下文詳情

因此咱們如今知道每次調用函數時都會建立一個新的執行上下文(execution context) 。可是,在JavaScript解釋器中,每次調用生成執行上下文(execution context)都有兩個階段:ecmascript

  1. 建立階段 [調用函數時,但在執行任何代碼以前]:
  • 建立做用域鏈
  • 建立變量(variables),函數(functions )和參數(arguments)
  • 肯定"this"。
  1. 激活/執行階段:
  • var 賦值,(function聲明)指向函數,解釋/執行代碼

能夠將每一個execution context概念上表示爲具備3個屬性的對象:函數

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

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

這executionContextObj是在調用函數時,但在執行實際函數以前建立的。這是第一階段:建立階段。這裏,解釋器經過掃描傳入的參數或arguments、本地函數聲明和局部變量聲明來建立executionContextObj。此次掃描的結果就變成了executionContextObj.variableObject。

如下是解釋器如何解析代碼的僞概述:

  1. 遇到函數調用。
  2. 在執行function代碼以前,建立執行上下文(execution context)。
  3. 進入建立階段:

    • 初始化做用域鏈(Scope Chain)。
    • 建立變量對象(variable object):

      • 建立arguments object,檢查參數的上下文,初始化名稱和值並建立引用副本。
      • 掃描上下文以獲取函數聲明:

        • 對於找到的每一個函數,在variable object中建立一個以函數名稱爲屬性的鍵值對,值指向內存中函數的引用指針。
        • 若是函數名已存在,則將覆蓋引用指針值。
      • 掃描上下文以獲取變量聲明:

        • 對於找到的每一個變量聲明,在variable object中建立一個以變量名爲屬性的鍵值對,值初始化爲undefined。
        • 若是變量名已經存在於variable object,則不執行任何操做並繼續掃描。
    • 肯定"this"在上下文中的值。
  4. 激活/執行階段:

    • 在上下文中運行/解析函數體的代碼,並在代碼逐行執行時爲變量賦值。

咱們來看一個例子:

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

在調用時foo(22),creation stage長這樣子:

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

正如您所看到的,creation stage定義屬性的name,不爲它們賦值,但formal arguments / parameters(函數傳參,arguments)除外。一旦creation stage完成後,執行流程進入函數體,在函數已經完成執行以後的execution stage以下:

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

提高

在不少JavaScript的資料中都提到了提高,解釋變量和函數聲明被提高到其做用域的頂部。可是,沒有人詳細解釋爲何會發生這種狀況,而在你掌握了關於解釋器如何建立activation object後,會很容易理解。示例:

(function() {

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

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

    function foo() {
        return 'hello';
    }

}());​

咱們如今能夠回答的問題是:

  • 爲何咱們能夠在聲明它以前訪問foo?

    • 若是咱們遵循creation stage,咱們知道變量在activation / code execution stage以前就建立了。因此當功能流程開始執行時,foo早就在activation object中定義了。
  • foo是聲明瞭兩次,爲何顯示foo的是 function ,__不是__ undefined string?

    • 即便foo聲明瞭兩次,咱們也知道在creation stage函數在變量以前就在activation objectbefore上建立了,若是屬性名已經存在於activation object,解釋器會忽略掉這次聲明。
    • 所以,首先會在activation object上建立一個foo()的引用,當解釋器到達時var foo,屬性名稱foo存在,因此代碼什麼也不作,而後繼續。
  • 爲何 bar 是 undefined?

    • bar其實是一個具備函數賦值的變量,咱們知道變量是在creation stage建立的,但它們的初始值爲undefined。

概要

但願到如今您已經很好地掌握了JavaScript解釋器如何執行您的代碼。理解執行上下文和隊列可讓您瞭解代碼沒有達到預期的緣由

您是否定爲了解解釋器的內部工做原理是您的JavaScript知識的重要組成部分?知道執行上下文的每一個階段是否有助於您編寫更好的JavaScript?

__注意__:有些人一直在問關於閉包,回調,超時等,我將在在下一篇文章中涉及,主要概述做用域鏈與execution context的關係。

拓展

相關文章
相關標籤/搜索