JavaScript中執行上下文和執行棧是什麼?

林恒發表於2024-05-16

一、執行上下文

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

執行上下文的型別分為三種:

  • 全域性執行上下文:只有一個,瀏覽器中的全域性物件就是 window物件,this 指向這個全域性物件
  • 函式執行上下文:存在無數個,只有在函式被呼叫的時候才會被建立,每次呼叫函式都會建立一個新的執行上下文
  • Eval 函式執行上下文: 指的是執行在 eval 函式中的程式碼,很少用而且不建議使用

下面給出全域性上下文和函式上下文的例子:

紫色框住的部分為全域性上下文,藍色和橘色框起來的是不同的函式上下文。只有全域性上下文(的變數)能被其他任何上下文訪問

可以有任意多個函式上下文,每次呼叫函式建立一個新的上下文,會建立一個私有作用域,函式內部宣告的任何變數都不能在當前函式作用域外部直接訪問

二、生命週期

執行上下文的生命週期包括三個階段:建立階段 → 執行階段 → 回收階段

建立階段

建立階段即當函式被呼叫,但未執行任何其內部程式碼之前

建立階段做了三件事:

  • 確定 this 的值,也被稱為 This Binding
  • LexicalEnvironment(詞法環境) 元件被建立
  • VariableEnvironment(變數環境) 元件被建立

虛擬碼如下:

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

This Binding

確定this的值我們前面講到,this的值是在執行的時候才能確認,定義的時候不能確認

詞法環境

詞法環境有兩個組成部分:

  • 全域性環境:是一個沒有外部環境的詞法環境,其外部環境引用為null,有一個全域性物件,this 的值指向這個全域性物件

  • 函式環境:使用者在函式中定義的變數被儲存在環境記錄中,包含了arguments 物件,外部環境的引用可以是全域性環境,也可以是包含內部函式的外部函式環境

虛擬碼如下:

GlobalExectionContext = {  // 全域性執行上下文
  LexicalEnvironment: {       // 詞法環境
    EnvironmentRecord: {     // 環境記錄
      Type: "Object",           // 全域性環境
      // 識別符號繫結在這裡 
      outer: <null>           // 對外部環境的引用
  }  
}

FunctionExectionContext = { // 函式執行上下文
  LexicalEnvironment: {     // 詞法環境
    EnvironmentRecord: {    // 環境記錄
      Type: "Declarative",      // 函式環境
      // 識別符號繫結在這裡      // 對外部環境的引用
      outer: <Global or outer function environment reference>  
  }  
}

變數環境

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

在 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);

執行上下文如下:

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>  
  }  
}

留意上面的程式碼,letconst定義的變數ab在建立階段沒有被賦值,但var宣告的變數從在建立階段被賦值為undefined

這是因為,建立階段,會在程式碼中掃描變數和函式宣告,然後將函式宣告儲存在環境中

但變數會被初始化為undefined(var宣告的情況下)和保持uninitialized(未初始化狀態)(使用letconst宣告的情況下)

這就是變數提升的實際原因

執行階段

在這階段,執行變數賦值、程式碼執行

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

回收階段

執行上下文出棧等待虛擬機器回收執行上下文

二、執行棧

執行棧,也叫呼叫棧,具有 LIFO(後進先出)結構,用於儲存在程式碼執行期間建立的所有執行上下文

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');

轉化成圖的形式

簡單分析一下流程:

  • 建立全域性上下文請壓入執行棧
  • first函式被呼叫,建立函式執行上下文並壓入棧
  • 執行first函式過程遇到second函式,再建立一個函式執行上下文並壓入棧
  • second函式執行完畢,對應的函式執行上下文被推出執行棧,執行下一個執行上下文first函式
  • first函式執行完畢,對應的函式執行上下文也被推出棧中,然後執行全域性上下文
  • 所有程式碼執行完畢,全域性上下文也會被推出棧中,程式結束

參考文獻

  • https://zhuanlan.zhihu.com/p/107552264

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

JavaScript中執行上下文和執行棧是什麼?

相關文章