理解Javascript中的執行上下文和執行棧

前言

本文3356字,閱讀大約需要9分鐘。

總括: 本文深入的講解了Javascript中的執行上下文和執行棧。

流水在碰到底處時纔會釋放活力。

正文

如果你是或者想成爲一名Javascript開發者,那就必須要知道Javascript內部是如何執行的。正確的理解Javascript中的執行上下文和執行棧對於理解其它Javascript概念(比如變量提升,作用域,閉包等)至關重要。

正確的去理解Javascript執行上下文和執行棧將會是你成爲一名更好的Javascript開發者。

不多廢話,我們現在就開始:)

什麼是執行上下文

簡單的來說,執行上下文是一種對Javascript代碼執行環境的一種抽象概念,也就是說只要有Javascript代碼運行,那麼它就一定是運行在執行上下文中。

執行上下文的類型

Javascript一共有三種執行上下文:

  • **全局執行上下文。**這是一個默認的或者說基礎的執行上下文,所有不在函數中的代碼都會在全局執行上下文中執行。它會做兩件事:創建一個全局的window對象(瀏覽器環境下),並將this的值設置爲該全局對象,另外一個程序中只能有一個全局上下文。
  • **函數執行上下文。**每次調用函數時,都會爲該函數創建一個執行上下文,每一個函數都有自己的一個執行上下文,但注意是該執行上下文是在函數被調用的時候纔會被創建。函數執行上下文會有很多個,每當一個執行上下文被創建的時候,都會按照他們定義的順序去執行相關代碼(這會在後面會說到)。
  • **Eval函數執行上下文。**在eval函數中執行的代碼也會有自己的執行上下文,但由於eval函數不會被經常用到,這裏就不做討論了。(譯者注eval函數容易導致惡意攻擊,並且運行代碼的速度比相應的替代方法慢,因爲不推薦使用)。

執行棧

執行棧,在其他編程語言中也被稱爲“調用棧”,這是一種後進先出(LIFO)的數據結構,被用來儲存在代碼運行階段創建的所有的執行上下文。

當Javascript引擎(**譯者注:**Javascript引擎是執行Javascript代碼的解釋器,一般被內嵌在瀏覽器中)開始執行你第一行Javascript腳本代碼的時候,它就會創建一個全局執行上下文然後將它壓到執行棧中。每當引擎碰到一個函數的時候,它就會創建一個函數執行上下文,然後將這個執行上下文壓到執行棧中。(**譯者注:**這種結構類似彈夾,執行棧就是彈夾,執行上下文是子彈,子彈被一個個壓入彈夾,當子彈發射的時候,最後一個進彈夾的子彈會被最先射出)。

引擎會執行位於執行棧棧頂的執行上下文(一般是函數執行上下文),當該函數執行結束後,對應的執行上下文就會被彈出,然後控制流程到達執行棧的下一個執行上下文。

結合下面的代碼來理解下:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

上述代碼的執行棧

上述代碼的執行棧

當上述代碼在瀏覽器中加載時,Javascript引擎首先創建一個全局執行上下文並將其壓入執行棧中,然後碰到first()函數被調用,此時再創建一個函數執行上下文壓入執行棧中。

second()函數在first()函數中被調用時,引擎再針對這個函數創建一個函數執行上下文將其壓入執行棧中,second函數執行完畢後,對應的函數執行上下文被推出執行棧銷燬,然後控制流程到下一個執行上下文也就是first函數。

first函數執行結束,first函數執行上下文也被推出,引擎控制流程到全局執行上下文,直到所有的代碼執行完畢,全局執行上下文也會被推出執行棧銷燬,然後程序結束。

執行上下文是如何創建的?

現在我們已經瞭解了Javascript引擎是如何去處理執行上下文的,那麼,執行上下文是如何創建的呢?

執行上下文的創建分爲兩個階段:

  1. 創建階段;
  2. 執行階段;

創建階段

執行上下文是在創建階段被創建的,創建階段包括以下幾個方面:

  1. 創建詞法環境;
  2. 創建變量環境;

因此執行上下文可以抽象爲下面的形式:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

詞法環境

ES6的官方文檔 把詞法環境定義爲:

詞法環境(Lexical Environments)是一種規範類型,用於根據ECMAScript代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯。詞法環境由一個環境記錄(Environment Record)和一個可能爲空的外部詞法環境(outer Lexical Environment)引用組成。

簡單來說,詞法環境就是一種標識符—變量映射的結構(這裏的標識符指的是變量/函數的名字,變量是對實際對象[包含函數和數組類型的對象]或基礎數據類型的引用)。

舉個例子,看看下面的代碼:

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}

上面代碼的詞法環境類似這樣:

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}

每一個詞法環境由下面三部分組成:

  1. 環境記錄;
  2. 外部環境引用;
  3. 綁定this;
環境記錄

所謂的環境記錄就是詞法環境中記錄變量和函數聲明的地方。

環境記錄也有兩種類型:

  1. **聲明類環境記錄。**顧名思義,它存儲的是變量和函數聲明,函數的詞法環境內部就包含着一個聲明類環境記錄。
  2. **對象環境記錄。**全局環境中的詞法環境中就包含的就是一個對象環境記錄。除了變量和函數聲明外,對象環境記錄還包括全局對象(瀏覽器的window對象)。因此,對於對象的每一個新增屬性(對瀏覽器來說,它包含瀏覽器提供給window對象的所有屬性和方法),都會在該記錄中創建一個新條目。

**注意:**對函數而言,環境記錄還包含一個arguments對象,該對象是個類數組對象,包含參數索引和參數的映射以及一個傳入函數的參數的長度屬性。舉個例子,一個arguments對象像下面這樣:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument 對象類似下面這樣
Arguments: { 0: 2, 1: 3, length: 2 }

譯者注:環境記錄對象在創建階段也被稱爲變量對象(VO),在執行階段被稱爲活動對象(AO)。之所以被稱爲變量對象是因爲此時該對象只是存儲執行上下文中變量和函數聲明,之後代碼開始執行,變量會逐漸被初始化或是修改,然後這個對象就被稱爲活動對象

外部環境引用

對於外部環境的引用意味着在當前執行上下文中可以訪問外部詞法環境。也就是說,如果在當前的詞法環境中找不到某個變量,那麼Javascript引擎會試圖在上層的詞法環境中尋找。(**譯者注:**Javascript引擎會根據這個屬性來構成我們常說的作用域鏈)

綁定this

在詞法環境創建階段中,會確定this的值。

在全局執行上下文中,this值會被映射到全局對象中(在瀏覽器中,也就是window對象)。

在函數執行上下文中,this值取決於誰調用了該函數,如果是對象調用了它,那麼就將this值設置爲該對象,否則將this值設置爲全局對象或是undefined(嚴格模式下)。例如:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 上面calcAge的 'this' 就是 'person',因爲calcAge是被person對象調用的
const calculateAge = person.calcAge;
calculateAge();
// 上面的'this' 指向全局對象(window),因爲沒有對象調用它,或者說是window調用了它(window省略不寫)

詞法環境抽象出來類似下面的僞代碼:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
     	// 標識符在這裏綁定
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 標識符在這裏綁定
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
   }
}

變量環境

其實變量環境也是詞法環境的一種,它的環境記錄包含了變量聲明語句在執行上下文中創建的變量和具體值的綁定關係。

如上所述,變量環境也是詞法環境的一種,因此它具有詞法環境所有的屬性。

在ES6中,詞法環境變量環境的不同就是前者用來存儲函數聲明和變量聲明(letconst)綁定關係,後者只用來存儲var聲明的變量綁定關係。

執行階段

在這個階段,將完成所有變量的賦值操作,然後執行代碼。

例子

我們看幾個例子來理解上面的概念:

let a = 20;
const b = 30;
var c;
function multiply(e, f) {
 var g = 20;
 return e * f * g;
}
c = multiply(20, 30);

當上面的代碼被執行的時候,Javascript引擎會創建一個全局執行上下文去執行全局的代碼。所以全局執行上下文在創建階段看起來會像下面這樣:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 標識符在這裏綁定
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 標識符在這裏綁定
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

在執行階段,將完成變量的賦值操作,因此在執行階段全局執行上下文看起來會像下面這樣:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 標識符在這裏綁定
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 標識符在這裏綁定
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

當調用multiply(20, 30)時,將爲該函數創建一個函數執行上下文,該函數執行上下文在創建階段像下面這樣:

FunctionExectionContext = {
	LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 標識符在這裏綁定
      Arguments: { 0: 20, 1: 30, length: 2 },
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
	VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
     	// 標識符在這裏綁定
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

然後,執行上下文進入執行階段,這時候已經完成了變量的賦值操作。該函數上下文在執行階段像下面這樣:

FunctionExectionContext = {
	LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 標識符在這裏綁定
      Arguments: { 0: 20, 1: 30, length: 2 },
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
	VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 標識符在這裏綁定
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

函數執行完成後,返回值存儲在變量c中,此時全局詞法環境被更新。之後,全局代碼執行完成,程序結束。

**注意:**你可能已經注意到上面代碼,letconst定義的變量ab在創建階段沒有被賦值,但var聲明的變量從在創建階段被賦值爲undefined

這是因爲,在創建階段,會在代碼中掃描變量和函數聲明,然後將函數聲明存儲在環境中,但變量會被初始化爲undefined(var聲明的情況下)和保持uninitialized(未初始化狀態)(使用letconst聲明的情況下)。

這就是爲什麼使用var聲明的變量可以在變量聲明之前調用的原因,但在變量聲明之前訪問使用letconst聲明的變量會報錯(TDZ)的原因。

這其實就是我們經常聽到的變量聲明提升。

注意:執行階段,如果Javascript引擎找不到letconst聲明的變量的值,也會被賦值爲undefined

結論

如上,我們講解了Javascript代碼是如何執行的,雖然說成爲一名優秀的Javascript開發者並不需要完全搞懂這些概念,但對上面的概念有深入的理解有助於我們去學習和理解其它概念,比如:變量聲明提升,閉包,作用域鏈等。


能力有限,水平一般,歡迎勘誤,不勝感激。

訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

前端進階學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章