JS中的執(zhí)行上下文(Execution Context)和棧(stack)

本文翻譯之 http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/

在這篇文章中,我將深入探討JavaScript中一個(gè)最基本的部分,即Execution Context。 在本文結(jié)束時(shí),您應(yīng)該更清楚地知道解釋器是怎么工作的,為什么某些函數(shù)/變量在聲明之前就可以使用以及它們的值是如何確定的。

一:什么是執(zhí)行上下文?

當(dāng)JavaScript代碼運(yùn)行的時(shí)候,確定它運(yùn)行所在的環(huán)境是非常重要的。運(yùn)行環(huán)境由下面三種不同的代碼類型確定

  • 全局代碼(Global Code):代碼首次執(zhí)行時(shí)候的默認(rèn)環(huán)境
  • 函數(shù)代碼(Function Code):每當(dāng)執(zhí)行流程進(jìn)入到一個(gè)函數(shù)體內(nèi)部的時(shí)候
  • Eval代碼(Eval Code):當(dāng)eval函數(shù)內(nèi)部的文本執(zhí)行的時(shí)候

您可以在網(wǎng)上找到大量關(guān)于scope的參考資料。為了更易于理解,我們將execution context簡單視為運(yùn)行當(dāng)前代碼的environment/scope。好了,話不多說,先讓我們看個(gè)例子,其中包含了global context和function/local context 代碼。

Example 1

在上圖中,我們有1個(gè)全局上下文(Global Context),使用紫色邊框表示;有3個(gè)不同的函數(shù)上下文(Function Context)由綠色,藍(lán)色,和橙色邊框表示。注意!全局上下文有且只有一個(gè),程序中其他任意的上下文都可以訪問全局上下文。
你可以擁有任意數(shù)量的函數(shù)上下文。每一次函數(shù)調(diào)用都會創(chuàng)建一個(gè)新的上下文,它會創(chuàng)建一個(gè)私有域,函數(shù)內(nèi)部做出的所有聲明都會放在這個(gè)私有域中,并且這些聲明在當(dāng)前函數(shù)作用域外無法直接訪問。在上面的例子中,一個(gè)函數(shù)可以訪問它所在的上下文尾部的變量,但是一個(gè)外部的上下文無法訪問內(nèi)部函數(shù)內(nèi)部聲明的變量/函數(shù)。為什么會發(fā)生這樣的情況?代碼究竟是如何被解析的呢?

二:執(zhí)行上下文棧

瀏覽器中的JS解釋器是單線程的。也就是說在瀏覽器中同一時(shí)間只能做一個(gè)事情,其他的action和event都會被排隊(duì)放入到執(zhí)行棧中(Execution Stack)。下圖表示了一個(gè)單線程棧的抽象視圖


Execution Context Stack

如我們所知,當(dāng)一個(gè)瀏覽器第一次load你的代碼的時(shí)候,首先它會進(jìn)入到一個(gè)全局執(zhí)行上下文中。如果在你的全局代碼中,你調(diào)用了一個(gè)函數(shù),那么程序的執(zhí)行流程會進(jìn)入到被調(diào)用的函數(shù)中,并創(chuàng)建一個(gè)新的執(zhí)行上下文,并將這個(gè)上下文推入到執(zhí)行棧頂。
如果在當(dāng)前的函數(shù)中,你由調(diào)用了一個(gè)函數(shù),那么也會執(zhí)行同樣的操作。執(zhí)行流程計(jì)入到剛被調(diào)用的函數(shù)內(nèi)部,重新創(chuàng)建一個(gè)新的執(zhí)行上下文,并再次推入到執(zhí)行棧頂。瀏覽器會一直執(zhí)行當(dāng)前棧頂?shù)膱?zhí)行上下文,一旦函數(shù)執(zhí)行完畢,該上下文就會被推出執(zhí)行棧。下面的例子展示了一個(gè)遞歸函數(shù)以及該程序的執(zhí)行棧:

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

這個(gè)代碼循環(huán)調(diào)用了三次,每次對i累加1。每次函數(shù)foo調(diào)用的時(shí)候,都會有一個(gè)創(chuàng)建新的執(zhí)行上下文。一旦上下文完成了執(zhí)行,就會推出棧,將控制流返回給它下面的執(zhí)行上下文,這樣一直到全局上下文。
關(guān)于執(zhí)行棧,有5點(diǎn)需要記?。?/p>

  • 單線程
  • 同步執(zhí)行
  • 一個(gè)全局上下文
  • 無數(shù)的函數(shù)上下文
  • 每次函數(shù)調(diào)用都會床架一個(gè)新的執(zhí)行上下文,即使是調(diào)用自身

三:執(zhí)行上下文詳解

我們已經(jīng)知道每當(dāng)一個(gè)函數(shù)調(diào)用發(fā)生,都會創(chuàng)建一個(gè)新的執(zhí)行上下文。但是在JS解釋器內(nèi)部,每次調(diào)用一個(gè)執(zhí)行上下文都分為兩個(gè)步驟

  1. 創(chuàng)建階段[在函數(shù)被調(diào)用,但還未執(zhí)行任何代碼之前]
  • 創(chuàng)建作用域鏈.
  • 創(chuàng)建變量,函數(shù)和參數(shù)
  • 決定"this"的值
  1. 激活/代碼執(zhí)行階段
  • 分配變量,以及到函數(shù)的引用,然后解析/執(zhí)行代碼

一個(gè)執(zhí)行上下文從概念上可以視為一個(gè)包含三個(gè)property的Object

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

四: Activation / Variable Object [AO/VO]

當(dāng)調(diào)用函數(shù)的時(shí)候,就會創(chuàng)建executionContextObj對象,此時(shí)真正的函數(shù)邏輯還未執(zhí)行。這就是第一階段---創(chuàng)建階段。在這里,解釋器會掃描函數(shù),根據(jù)獲取到的參數(shù)/傳參內(nèi)部函數(shù)聲明/內(nèi)部變量聲明,來創(chuàng)建executionContextObj對象。掃描的結(jié)果存放在executionContextObj對象的variableObject屬性中。

下面是解釋器解析代碼的流程概述:

  1. 找到被調(diào)用函數(shù)的代碼內(nèi)容
  2. 在執(zhí)行function代碼前,先創(chuàng)建執(zhí)行上下文execution context
  3. 進(jìn)入創(chuàng)建階段
    • 初始化 作用域鏈.
    • 創(chuàng)建 variable object:
      • 創(chuàng)建 arguments object;檢查上下文獲取入?yún)?,初始化形參名稱和數(shù)值,并創(chuàng)建一個(gè)引用拷貝
      • 掃描上下文獲取內(nèi)部函數(shù)聲明:
        • 對發(fā)現(xiàn)的每一個(gè)內(nèi)部函數(shù),都在variable object中創(chuàng)建一個(gè)和函數(shù)名一樣的property,該property作為一個(gè)引用指針指向函數(shù)代碼在內(nèi)存中的地址
        • 如果在variable object中已經(jīng)存在相同名稱的property,那么相應(yīng)的property會被重寫
      • 掃描上下文獲取內(nèi)部變量聲明:
        • 對發(fā)現(xiàn)的每一個(gè)內(nèi)部變量聲明,都在variable object中創(chuàng)建一個(gè)和變量名一樣的property,并且將其初始化為 undefined
        • 如果在variable object中已經(jīng)存在相同變量名稱的property,那么就跳過,不做任何動作,繼續(xù)掃描
    • 決定在上下文中"this" 的值
  4. 激活/代碼執(zhí)行階段:
    • 執(zhí)行上下文中的函數(shù)代碼,逐行運(yùn)行JS代碼,并給變量賦值

讓我們看個(gè)例子

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

    };
    function c() {

    }
}

foo(22);

當(dāng)剛調(diào)用foo(22)函數(shù)的時(shí)候,創(chuàng)建階段的上下文大致是下面的樣子:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {  // 創(chuàng)建了參數(shù)對象
            0: 22,
            length: 1
        },
        i: 22,  // 檢查上下文,創(chuàng)建形參名稱,賦值/或創(chuàng)建引用拷貝
        c: pointer to function c()  // 檢查上下文,發(fā)現(xiàn)內(nèi)部函數(shù)聲明,創(chuàng)建引用指向函數(shù)體
        a: undefined,  // 檢查上下文,發(fā)現(xiàn)內(nèi)部聲明變量a,初始化為undefined
        b: undefined   // 檢查上下文,發(fā)現(xiàn)內(nèi)部聲明變量b,初始化為undefined,此時(shí)并不賦值,右側(cè)的函數(shù)作為賦值語句,在代碼未執(zhí)行前,并不存在
    },
    this: { ... }
}

參見代碼中的備注,在創(chuàng)建階段除了形參參數(shù)進(jìn)行了定義和賦值外,其他只定義了property的名稱,并沒有賦值。一旦創(chuàng)建階段完成,執(zhí)行流程就進(jìn)入到函數(shù)內(nèi)部進(jìn)入激活/代碼執(zhí)行階段。在執(zhí)行完后的上下文大致如下:

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

五:關(guān)于提升(Hoisting)

網(wǎng)上有很多資源會提到JS特有的變量提升(Hoisting),其中會解釋說JS會將變量和函數(shù)聲明提升到函數(shù)作用域的頂部。但是,并沒有人詳細(xì)解釋為什么會出現(xiàn)這種情況。在掌握了關(guān)于解釋器如何創(chuàng)建上下文的知識后,這就非常容易解釋了??聪旅娴拇a:

?(function() {

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

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

    function foo() {
        return 'hello';
    }
    console.log(typeof foo); // string
}());?

我們現(xiàn)在可以回答的問題是:

  • 為什么我們可以在聲明foo之前就訪問它?

    • 如果我們遵循creation stage,我們知道變量在activation / code execution stage之前就創(chuàng)建了。所以當(dāng)功能流程開始執(zhí)行時(shí),在上下文中,foo已經(jīng)做了定義。
  • foo是聲明了兩次,為什么顯示foo的是 function ,不 undefined 還是 string?

    • 即使foo聲明了兩次,我們也知道在creation stage階段,在上下文中,函數(shù)是在變量之前創(chuàng)建的,并且如果上下文中一個(gè)變量名稱的屬性名已經(jīng)存在,我們就會忽略掉這個(gè)變量聲明。
    • 因此,function foo()首先在上下文中創(chuàng)建一個(gè)名為foo的引用property,當(dāng)解釋器到達(dá)時(shí)var foo時(shí),我們看到屬性名稱foo已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行。
  • 為什么bar是 undefined?

    • bar實(shí)際上是一個(gè)具有函數(shù)賦值的變量,我們知道變量是在creation stage階段創(chuàng)建的,但它們是用值會被初始化為undefined。
  • 為什么最后foo是string?

    • foo在創(chuàng)造階段按照規(guī)則被賦予了function的類型,但在執(zhí)行階段,隨著var foo = 'hello'的執(zhí)行,將其變?yōu)榱薙tring類型,下面的函數(shù)聲明在創(chuàng)造階段已經(jīng)執(zhí)行,因此跳過后,foo還是String類型

六:總結(jié)

希望到現(xiàn)在您已經(jīng)很好地掌握了JavaScript中解釋器是如何處理您的代碼的。理解執(zhí)行上下文和??梢宰屇私鉃槭裁创a運(yùn)行的結(jié)果和你最初預(yù)期的不同的原因。

進(jìn)一步閱讀:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 30,260評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,671評論 1 32
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,602評論 19 139
  • 前兩天,我遇上了這么一件事。 不知道是誰放了4件貨在我店面前方的空白處,當(dāng)我看到自己的地方被占用的時(shí)候,很憤怒?!?..
    張雪柔Gina閱讀 370評論 1 0
  • Day1 日期:十·一 坐標(biāo):河北安新,目的地:陜西西安。 都說十一假期出游便是注定了要觀人山人?!艺f,縱須閱人...
    青雨禪心閱讀 440評論 0 0

友情鏈接更多精彩內(nèi)容