本文主要介紹JavaScript程序內部的執行機制
首先先了解什么是執行上下文
執行上下文就是當前JavaScript代碼被解析和執行是所在環境的抽象概念,JavaScript中運行任何的代碼都是在執行上下文中運行。
執行上下文的類型,總共有三類
- 全局執行上下文:這是默認的,最基礎的執行上下文。不在任何函數中的代碼都位於全局執行上下文中。共有兩個過程:1.創建有全局對象,在瀏覽器中這個全局對象就是window對象。2.將this指針指向這個全局對象。一個程序中只能存在一個執行上下文。
- 函數執行上下文:每次調用函數時,都會為該函數創建一個新的執行上下文。每個函數都擁有自己的執行上下文,但是只有在函數被調用的時候才會被創建。一個程序中可以存在多個函數執行上下文,這些函數執行上下文按照特定的順序執行一系列步驟,后文具體討論。
- Eval函數執行上下文:運行在eval函數中的代碼也獲得了自己的執行上下文,但由於Eval較為少用到,也不建議使用,就不去詳細討論了。。。(eval方法是在運行時對腳本進行解釋執行,而普通的javascript會有一個預處理的過程。所以會有一些性能上的損失;eval也存在一個安全問題,因為它可以執行傳給它的任何字符串,所以永遠不要傳入字符串或者來歷不明和不受信任源的參數。
執行棧
執行棧,也叫調用棧,具有LIFO(Last in, First out 后進先出)結構,用於存儲在代碼執行期間創建的所有執行上下文。
當JavaScript引擎首次讀取腳本時,會創建一個全局執行上下文並將其Push到當前執行棧中。每當發生函數調用時,引擎都會為該函數創建一個新的執行上下文並Push到當前執行棧的棧頂。
引擎會運行執行上下文在執行棧棧頂的函數,根據LIFO規則,當此函數運行完成后,其對應的執行上下文將會從執行棧中Pop出,上下文控制權將轉到當前執行棧的下一個執行上下文。
通過一下代碼來更好理解:
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()
函數時,JavaScript 引擎為該函數創建了一個新的執行上下文並將其推到當前執行棧的頂端。
當在 first()
函數中調用 second()
函數時,Javascript 引擎為該函數創建了一個新的執行上下文並將其推到當前執行棧的頂端。當 second()
函數執行完成后,它的執行上下文從當前執行棧中彈出,上下文控制權將移到當前執行棧的下一個執行上下文,即 first()
函數的執行上下文。
當 first()
函數執行完成后,它的執行上下文從當前執行棧中彈出,上下文控制權將移到全局執行上下文。一旦所有代碼執行完畢,Javascript 引擎把全局執行上下文從執行棧中移除。
執行上下文是如何被創建的
到目前為止,我們已經看到了 JavaScript 引擎如何管理執行上下文,現在就讓我們來理解 JavaScript 引擎是如何創建執行上下文的。
執行上下文分兩個階段創建:1)創建階段; 2)執行階段
創建階段
在任意的JavaScript代碼被執行前,執行上下文處於創建階段。在創建階段總共發生了三件事情:
- 確定this的值,也被稱為This Binding;
- LexicaEnvironment(詞法環境)組件被創建;
- VariableEnvironment(變量環境)組件被創建。
因此,執行上下文可以在概念上表示如下:
ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, }
This Biling:
在全局執行上下文中,this
的值指向全局對象,在瀏覽器中,this
的值指向 window 對象。
在函數執行上下文中,this
的值取決於函數的調用方式。如果它被一個對象引用調用,那么 this
的值被設置為該對象,否則 this
的值被設置為全局對象或 undefined
(嚴格模式下)。例如:
let person = { name: 'peter', birthYear: 1994, calcAge: function() { console.log(2018 - this.birthYear); } } person.calcAge(); // 'this' 指向 'person', 因為 'calcAge' 是被 'person' 對象引用調用的。 let calculateAge = person.calcAge; calculateAge(); // 'this' 指向全局 window 對象,因為沒有給出任何對象引用
詞法環境(Lexical Environment):
官方ES6文檔將詞法環境定義為:
- 詞法環境是一種規范類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關系。詞法環境由環境記錄(environment record)和可能為空引用(null)的外部詞法環境組成。
簡而言之,詞法環境是一個包含標識符變量映射的結構。(這里的標識符表示變量/函數的名稱,變量是對實際對象【包括函數類型對象】或原始值的引用)
在詞法環境中,有兩個組成部分:(1)環境記錄(environment record) (2)對外部環境的引用
2. 對外部環境的引用意味着它可以訪問其外部詞法環境
- 全局環境(在全局執行上下文中)是一個沒有外部環境詞法環境的詞法環境。全局環境的外部環境引用為null。它擁有一個全局對象(window對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this的值指向這個全局對象。
- 函數環境,用戶在函數中定義的變量被存儲在環境記錄中,包含了arguments對象。對外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境。
function foo(a, b) { var c = a + b; } foo(2, 3); // arguments 對象 Arguments: {0: 2, 1: 3, length: 2},
環境記錄同樣也有兩種類型:
- 聲明性環境記錄存儲變量,函數和參數。一個函數環境包含聲明性環境記錄。
- 對象環境記錄用於定義在全局執行上下文中出現的變量和函數的關聯。全局環境包含對象環境記錄。
抽象地說,詞法環境的偽代碼中看起來像這樣:
GlobalExectionContext = { // 全局執行上下文 LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { // 環境記錄 Type: "Object", //全局環境 // 標識符綁定在這里 outer: <null> // 對外部環境的引用 } } FunctionExectionContext = { //函數執行上下文 LexicalEnvironment: { //詞法環境 EnvironmentRecord: { //環境記錄 Type: "Declarative", //函數環境 // 標識符綁定在這里 outer: <Global or outer function environment reference> //對外部環境的引用 } }
變量環境:
變量環境是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性。
在 ES6 中,詞法環境(LexicalEnvironment 組件)和 變量環境(VariableEnvironment 組件)的區別在於前者用於存儲函數聲明和變量( let
和 const
)綁定,而后者僅用於存儲變量( var
)綁定。
使用例子進行介紹:
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
執行上下文如下:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這里 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這里 g: undefined }, outer: <GlobalLexicalEnvironment> } }
注意:只有在遇到函數multiply的調用時才會創建函數執行上下文。
變量提升的原因:在創建階段,函數聲明存儲在環境中,而變量會被設置為 undefined
(在 var
的情況下)或保持未初始化(在 let
和 const
的情況下)。所以這就是為什么可以在聲明之前訪問 var
定義的變量(盡管是 undefined
),但如果在聲明之前訪問 let
和 const
定義的變量就會提示引用錯誤的原因。這就是所謂的變量提升。
執行階段
此階段,完成對所有變量的分配,最后執行代碼。
如果JavaScript引擎在源代碼中聲明的實際位置找不到 let 變量的值,那么將為其分配 undefined 值。
參考: