JavaScript進階-執行上下文(理解執行上下文一篇就夠了)

LinDaiDai_霖呆呆發表於2019-10-29

前言

在程式設計這個行業中總是能聽到這個詞執行上下文。那麼什麼叫執行上下文呢?

本篇文章主要是介紹javascript中的執行上下文, 看完之後你可以瞭解到:

  • 執行上下文的型別
  • 執行上下文特點
  • 執行棧
  • 執行上下文的生命週期

概念

首先我們來介紹什麼是“執行上下文”.

舉個例子,生活中,相同的話在不同的場合說可能會有不同的意思,而這個說話的場合就是我們說話的語境。

同樣對應在程式設計中, 對程式語言進行“解讀”的時候,也必須在特定的語境中,這個語境就是javascript中的執行上下文。

一句話概括:

執行上下文就是javascript程式碼被解析和執行時所在環境的抽象概念。

執行上下文的型別

js中,執行上下文分為以下三種:

  • 全域性執行上下文:只有一個,也就是瀏覽器物件(即window物件),this指向的就是這個全域性物件。
  • 函式執行上下文:有無數個,只有在函式被呼叫時才會被建立,每次呼叫函式都會建立一個新的執行上下文。
  • Eval函式執行上下文jseval函式執行其內部的程式碼會建立屬於自己的執行上下文, 很少用而且不建議使用。

執行上下文的特點

  1. 單執行緒,只在主執行緒上執行;
  2. 同步執行,從上向下按順序執行;
  3. 全域性上下文只有一個,也就是window物件;
  4. 函式執行上下文沒有限制;
  5. 函式每呼叫一次就會產生一個新的執行上下文環境。

JS如何管理多個執行上下文

通過上面介紹,我們知道了js程式碼在執行時可能會產生無數個執行上下文,那麼它是如何管理這些執行上下文的呢?

同時由於js是單執行緒的,所以不能同時幹兩件事,必須一個個去執行,那麼這麼多的執行上下文是按什麼順序執行的呢?

執行棧

接下來就對上面的問題做出解答,管理多個執行上下文靠的就是執行棧,也被叫做呼叫棧

特點:後進先出(LIFO)的結構。

作用:儲存在程式碼執行期間的所有執行上下文。

LIFO: last-in, first-out,類似於向乒乓球桶中放球,最先放入的球最後取出)

js在首次執行的時候,會建立一個全域性執行上下文並推入棧中。

每當有函式被呼叫時,引擎都會為該函式建立一個新的函式執行上下文然後推入棧中。

當棧頂的函式執行完畢之後,該函式對應的執行上下文就會從執行棧中pop出,然後上下文控制權移到下一個執行上下文。

比如下面的一個例子?:

var a = 1; // 1. 全域性上下文環境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文環境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文環境
複製程式碼

如下圖:

context1

執行上下文的生命週期

執行上下文的生命週期也非常容易理解, 分為三個階段:

  1. 建立階段
  2. 執行階段
  3. 銷燬階段

建立階段

建立階段, 主要有是有這麼幾件事:

  1. 確定this的值, 也就是繫結this (This Binding);
  2. 詞法環境(LexicalEnvironment)元件被建立;
  3. **變數環境(VariableEnvironment)**元件被建立.

一張圖方便你理解 ?

executionContext1

有一些教材中也喜歡用虛擬碼來實現:

ExecutionContext = {  
  ThisBinding = <this value>,     // 確定this 
  LexicalEnvironment = { ... },   // 詞法環境
  VariableEnvironment = { ... },  // 變數環境
}
複製程式碼

This Binding

通過上面的介紹我們知道實際開發主要用到兩種執行上下文為全域性函式, 那麼繫結this在這兩種上下文中也不同.

  • 全域性執行上下文中, this指的就是全域性物件, 瀏覽器環境指向window物件, nodejs中指向這個檔案的module物件.
  • 函式執行上下文較為複雜, this的值取決於函式的呼叫方式. 具體有: 預設繫結、隱式繫結、顯式繫結、new繫結、箭頭函式.

詞法環境

如上圖, 詞法環境是由兩個部分組成的:

  1. 環境記錄: 儲存變數和函式宣告的實際位置;
  2. 對外部環境的引用: 用於訪問其外部詞法環境.

同樣的, 詞法環境也主要有兩種型別:

  1. 全域性環境: 擁有一個全域性物件(window物件)及其關聯的所有屬性和方法(比如陣列的方法splice、concat等), 同時也包含了使用者自定義的全域性變數. 但是全域性環境中沒有外部環境的引用, 也就是外部環境引用為null.
  2. 函式環境: 使用者在函式中自定義的變數和函式儲存在環境記錄中, 包含了arguments物件. 而對外部環境的引用可以是全域性環境, 也可以是另一個函式環境(比如一個函式中包含了另一個函式).

繼續用虛擬碼來實現:

GlobalExectionContext = { // 全域性執行上下文
    LexicalEnvironment: {   // 詞法環境
        EnvironmentRecord: {   // 環境記錄
            Type: "Object"       // 全域性環境
            // 識別符號繫結在這裡
        },
        outer: <null>          // 外部環境引用
    }
}
FunctionExectionContext = { // 函式執行上下文
    LexicalEnvironment: {   // 詞法環境
        EnvironmentRecord: {   // 環境記錄
            Type: "Object",       // 函式環境
            // 識別符號繫結在這裡
        },
    outer: < Global or FunctionEnvironment> // 外部環境引用
    }
}
複製程式碼

變數環境

變數環境其實也是一個詞法環境, 因此它具有上面定義的詞法環境的所有屬性.

在 ES6 中,詞法 環境和 變數 環境的區別在於前者用於儲存**函式宣告和變數( letconst繫結,而後者僅用於儲存變數( var )**繫結。

案例?:

var a;
var b = 1;
let c = 2;
const d = 3;
function fn (e, f) {
    var g = 4;
    return e + f + g;
}
a = fn(10, 20);
複製程式碼

執行上下文如下:

GlobalExectionContext = { // 全域性執行上下文
    ThisBinding: <Global Object>,
    LexicalEnvironment: {   // 詞法環境
    	EnvironmentRecord: {   // 環境記錄
        	Type: "Object",       // 全域性環境
        	c: < uninitialized >,
                d: < uninitialized >,
        	fn: < func >
    	},
    	outer: <null>            // 外部環境引用
    },
    VariableEnvironment: {   // 變數環境
    	EnvironmentRecord: {   // 環境記錄
    		Type: "Object",
    		a: < uninitialized >,
    		b: < uninitialized >
    	},
    	outer: <null>  
    }
}
FunctionExectionContext = { // 函式執行上下文
    ThisBinding: <Global Object>, // this繫結window, 因為呼叫fn的是window物件
    LexicalEnvironment: {   // 詞法環境
    	EnvironmentRecord: {   // 環境記錄
    		Type: "Object",       // 函式環境
    		Arguments: { 0: 10, 1: 20, length: 2 }
    	},
        outer: < GlobalLexicalEnvironment > // 全域性環境的引用
    },
    VariableEnvironment: {   // 變數環境
    	EnvironmentRecord: {   // 環境記錄
    		Type: "Object",
    		g: < uninitialized >
    	},
    	outer: < GlobalLexicalEnvironment > // 全域性環境的引用
    }
}
複製程式碼

因此我們可以知道變數提升的原因是:

在建立階段,函式宣告儲存在環境中,而變數會被設定為 undefined(在 var 的情況下)或保持未初始化(在 letconst 的情況下)。所以這就是為什麼可以在宣告之前訪問 var 定義的變數(儘管是 undefined ),但如果在宣告之前訪問 letconst 定義的變數就會提示引用錯誤的原因。這就是所謂的變數提升。

執行階段

執行階段主要做三件事情:

  1. 變數賦值
  2. 函式引用
  3. 執行其他的程式碼

注⚠️

如果 Javascript 引擎在原始碼中宣告的實際位置找不到 let 變數的值,那麼將為其分配 undefined 值。

銷燬階段

執行完畢出棧,等待回收被銷燬

後語

該篇文章僅僅只是對執行上下文做一個入門程度的介紹, 後面會深入介紹它.

參考文章:

木易楊前端進階-理解JavaScript 中的執行上下文和執行棧

JS執行上下文

相關文章