了解JavaScript的執(zhí)行上下文

2018-06-16 20:16 更新

在這篇文章里,我將深入研究JavaScript中最基本的部分——執(zhí)行上下文(execution context)。讀完本文后,你應(yīng)該清楚了解解釋器做了什么,為什么函數(shù)和變量能在聲明前使用以及他們的值是如何決定的。

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

當(dāng)JavaScript代碼運(yùn)行,執(zhí)行環(huán)境非常重要,有下面幾種不同的情況:

  • 全局代碼——你的代碼首次執(zhí)行的默認(rèn)環(huán)境。
  • 函數(shù)代碼——每當(dāng)進(jìn)入一個函數(shù)內(nèi)部。
  • Eval代碼——eval內(nèi)部的文本被執(zhí)行時(shí)。

在網(wǎng)上你能讀到許多關(guān)于作用域(scope)的資源,本文的目的是讓事情變得更簡單,讓我們將術(shù)語執(zhí)行上下文想象為當(dāng)前被執(zhí)行代碼的環(huán)境/作用域。說的夠多了,現(xiàn)在讓我們看一個包含全局和函數(shù)上下文的代碼例子。

很簡單的例子,我們有一個被紫色邊框圈起來的全局上下文和三個分別被綠色,藍(lán)色和橘色框起來的不同函數(shù)上下文。只有全局上下文(的變量)能被其他任何上下文訪問。

你可以有任意多個函數(shù)上下文,每次調(diào)用函數(shù)創(chuàng)建一個新的上下文,會創(chuàng)建一個私有作用域,函數(shù)內(nèi)部聲明的任何變量都不能在當(dāng)前函數(shù)作用域外部直接訪問。在上面的例子中,函數(shù)能訪問當(dāng)前上下文外面的變量聲明,但在外部上下文不能訪問內(nèi)部的變量/函數(shù)聲明。為什么會發(fā)生這種情況?代碼到底是如何被解釋的?

執(zhí)行上下文堆棧

瀏覽器里的JavaScript解釋器被實(shí)現(xiàn)為單線程。這意味著同一時(shí)間只能發(fā)生一件事情,其他的行文或事件將會被放在叫做執(zhí)行棧里面排隊(duì)。下面的圖是單線程棧的抽象視圖:

我們已經(jīng)知道,當(dāng)瀏覽器首次載入你的腳本,它將默認(rèn)進(jìn)入全局執(zhí)行上下文。如果,你在你的全局代碼中調(diào)用一個函數(shù),你程序的時(shí)序?qū)⑦M(jìn)入被調(diào)用的函數(shù),并穿件一個新的執(zhí)行上下文,并將新創(chuàng)建的上下文壓入執(zhí)行棧的頂部。

如果你調(diào)用當(dāng)前函數(shù)內(nèi)部的其他函數(shù),相同的事情會在此上演。代碼的執(zhí)行流程進(jìn)入內(nèi)部函數(shù),創(chuàng)建一個新的執(zhí)行上下文并把它壓入執(zhí)行棧的頂部。瀏覽器將總會執(zhí)行棧頂?shù)膱?zhí)行上下文,一旦當(dāng)前上下文函數(shù)執(zhí)行結(jié)束,它將被從棧頂彈出,并將上下文控制權(quán)交給當(dāng)前的棧。下面的例子顯示遞歸函數(shù)的執(zhí)行棧調(diào)用過程:

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

這代碼調(diào)用自己三次,每次給i的值加一。每次foo函數(shù)被調(diào)用,將創(chuàng)建一個新的執(zhí)行上下文。一旦上下文執(zhí)行完畢,它將被從棧頂彈出,并將控制權(quán)返回給下面的上下文,直到只剩全局上下文能為止。

有5個需要記住的關(guān)鍵點(diǎn),關(guān)于執(zhí)行棧(調(diào)用棧):

  • 單線程。
  • 同步執(zhí)行。
  • 一個全局上下文。
  • 無限制函數(shù)上下文。
  • 每次函數(shù)被調(diào)用創(chuàng)建新的執(zhí)行上下文,包括調(diào)用自己。

執(zhí)行上下文的細(xì)節(jié)

我們現(xiàn)在已經(jīng)知道沒次調(diào)用函數(shù),都會創(chuàng)建新的執(zhí)行上下文。然而,在JavaScript解釋器內(nèi)部,每次調(diào)用執(zhí)行上下文,分為兩個階段:

  1. 創(chuàng)建階段【當(dāng)函數(shù)被調(diào)用,但未執(zhí)行任何其內(nèi)部代碼之前】:
    • 創(chuàng)建作用域鏈(Scope Chain
    • 創(chuàng)建變量,函數(shù)和參數(shù)。
    • 求”this“的值。
  2. 激活/代碼執(zhí)行階段:
    • 指派變量的值和函數(shù)的引用,解釋/執(zhí)行代碼。

可以將每個執(zhí)行上下文抽象為一個對象并有三個屬性:

executionContextObj = {
    scopeChain: { /* 變量對象(variableObject)+ 所有父執(zhí)行上下文的變量對象*/ }, 
    variableObject: { /*函數(shù) arguments/參數(shù),內(nèi)部變量和函數(shù)聲明 */ }, 
    this: {} 
}

激活/變量對象【AO/VO】

當(dāng)函數(shù)被調(diào)用是executionContextObj被創(chuàng)建,但在實(shí)際函數(shù)執(zhí)行之前。這是我們上面提到的第一階段,創(chuàng)建階段。在此階段,解釋器掃描傳遞給函數(shù)的參數(shù)或arguments,本地函數(shù)聲明和本地變量聲明,并創(chuàng)建executionContextObj對象。掃描的結(jié)果將完成變量對象的創(chuàng)建。

下面是解釋器如果執(zhí)行代碼的偽邏輯:

  1. 查找調(diào)用函數(shù)的代碼。
  2. 執(zhí)行函數(shù)代碼之前,先創(chuàng)建執(zhí)行上下文。
  3. 進(jìn)入創(chuàng)建階段:
    • 初始化作用域鏈:
    • 創(chuàng)建變量對象:
      • 創(chuàng)建arguments對象,檢查上下文,初始化參數(shù)名稱和值并創(chuàng)建引用的復(fù)制。
      • 掃描上下文的函數(shù)聲明:
        • 為發(fā)現(xiàn)的每一個函數(shù),在變量對象上創(chuàng)建一個屬性——確切的說是函數(shù)的名字——其有一個指向函數(shù)在內(nèi)存中的引用。
        • 如果函數(shù)的名字已經(jīng)存在,引用指針將被重寫。
      • 掃面上下文的變量聲明:
        • 為發(fā)現(xiàn)的每個變量聲明,在變量對象上創(chuàng)建一個屬性——就是變量的名字,并且將變量的值初始化為undefined
        • 如果變量的名字已經(jīng)在變量對象里存在,將不會進(jìn)行任何操作并繼續(xù)掃描。
    • 求出上下文內(nèi)部“this”的值。
  4. 激活/代碼執(zhí)行階段:
    • 在當(dāng)前上下文上運(yùn)行/解釋函數(shù)代碼,并隨著代碼一行行執(zhí)行指派變量的值。

讓我們看一個例子:

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

    };
    function c() {

    }
}

foo(22);

當(dāng)調(diào)用foo(22)時(shí),創(chuàng)建狀態(tài)像下面這樣:

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

真如你看到的,創(chuàng)建狀態(tài)負(fù)責(zé)處理定義屬性的名字,不為他們指派具體的值,以及形參/實(shí)參的處理。一旦創(chuàng)建階段完成,執(zhí)行流進(jìn)入函數(shù)并且激活/代碼執(zhí)行階段,看下函數(shù)執(zhí)行完成后的樣子:

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

提升(Hoisting)

你能在網(wǎng)上找到很多定義JavaScript hoisting術(shù)語的資源,解釋變量和函數(shù)聲明被提升到函數(shù)作用域的頂部。然而,沒有人解釋為什么會發(fā)生這種情況的細(xì)節(jié),學(xué)習(xí)了上面關(guān)于解釋器如何創(chuàng)建愛你活動對象的新知識,很容易明白為什么。看下面的例子:

(function() {

    console.log(typeof foo); // 函數(shù)指針
    console.log(typeof bar); // undefined

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

    function foo() {
        return 'hello';
    }

}()); ? 我們能回答下面的問題:
  • 為什么我們能在foo聲明之前訪問它?
    • 如果我們跟隨創(chuàng)建階段,我們知道變量在激活/代碼執(zhí)行階段已經(jīng)被創(chuàng)建。所以在函數(shù)開始執(zhí)行之前,foo已經(jīng)在活動對象里面被定義了。
  • Foo被聲明了兩次,為什么foo顯示為函數(shù)而不是undefined或字符串?
    • 盡管foo被聲明了兩次,我們知道從創(chuàng)建階段函數(shù)已經(jīng)在活動對象里面被創(chuàng)建,這一過程發(fā)生在變量創(chuàng)建之前,并且如果屬性名已經(jīng)在活動對象上存在,我們僅僅更新引用。
    • 因此,對foo()函數(shù)的引用首先被創(chuàng)建在活動對象里,并且當(dāng)我們解釋到var foo時(shí),我們看見foo屬性名已經(jīng)存在,所以代碼什么都不做并繼續(xù)執(zhí)行。
  • 為什么bar的值是undefined?
    • bar實(shí)際上是一個變量,但變量的值是函數(shù),并且我們知道變量在創(chuàng)建階段被創(chuàng)建但他們被初始化為undefined。

總結(jié)

希望現(xiàn)在你了解JavaScript解釋器如何執(zhí)行你的代碼。了解執(zhí)行上下文和堆棧,將有助于你了解背后的原因——為什么你的代碼被解釋為和你最初希望不同的值。

你想知道解釋器內(nèi)部的運(yùn)作的開銷太大,或者你的JavaScript知識的必要性?知道執(zhí)行上下文相幫你寫出更好的JavaScript?

你想知道解釋器的內(nèi)部工作原理,需要太多篇幅,和必要的JavaScript知識。知道執(zhí)行上下文能幫你寫出更好的JavaScript代碼。

注意:有些人一直在問閉包,回調(diào),延時(shí)等問題,我將在下一篇文章里提到,更多關(guān)注域執(zhí)行上下文有關(guān)的作用域鏈相關(guān)方面。

深入閱讀

原文:http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號