JavaScript執行上下文-執行棧

前言

忽然以爲對於一名JavaScript開發者而言,須要知道JavaScript程序內部是如何運行的,那麼對於此章節執行上下文和執行棧的理解很重要,對理解其餘JavaScript概念(變量聲明提示,做用域和閉包)都有幫助。javascript

看了不少相關文章,寫得很好,總結了ES3以及ES6對於執行上下文概念的描述,以及新的概念介紹。java

什麼是執行上下文

簡而言之,執行上下文是評估和執行 JavaScript 代碼的環境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執行上下文中運行。node

執行上下文的類型

JavaScript 中有三種執行上下文類型express

  • 全局執行上下文 — 這是默認或者說基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:建立一個全局的 window 對象(瀏覽器的狀況下),而且設置 this 的值等於這個全局對象。一個程序中只會有一個全局執行上下文。
  • 函數執行上下文 — 每當一個函數被調用時, 都會爲該函數建立一個新的上下文。每一個函數都有它本身的執行上下文,不過是在函數被調用時建立的。函數上下文能夠有任意多個。每當一個新的執行上下文被建立,它會按定義的順序(將在後文討論)執行一系列步驟。
  • Eval 函數執行上下文 — 執行在 eval 函數內部的代碼也會有它屬於本身的執行上下文,但因爲 JavaScript 開發者並不常用 eval,因此在這裏我不會討論它。

ES3 執行上下文的內容

執行上下文是一個抽象的概念,咱們能夠將它理解爲一個 object ,一個執行上下文裏包括如下內容:編程

  1. 變量對象 VO
  2. 活動對象 AO
  3. 做用域鏈
  4. 調用者信息 this

變量對象(variable object 簡稱 VO

每一個執行環境文都有一個表示變量的對象——變量對象,全局執行環境的變量對象始終存在,而函數這樣局部環境的變量,只會在函數執行的過程當中存在,在函數被調用時且在具體的函數代碼運行以前,JS 引擎會用當前函數的參數列表arguments)初始化一個 「變量對象」 並將當前執行上下文與之關聯 ,函數代碼塊中聲明的 變量函數 將做爲屬性添加到這個變量對象上。數組

有一點須要注意,只有函數聲明(function declaration)會被加入到變量對象中,而函數表達式(function expression)會被忽略。
複製代碼
// 這種叫作函數聲明,會被加入變量對象
function demo () {}
// tmp 是變量聲明,也會被加入變量對象,可是做爲一個函數表達式 demo2 不會被加入變量對象
var tmp = function demo2 () {}
複製代碼

全局執行上下文和函數執行上下文中的變量對象還略有不一樣,它們之間的差異簡單來講:瀏覽器

  1. 全局上下文中的變量對象就是全局對象,以瀏覽器環境來講,就是 window 對象。
  2. 函數執行上下文中的變量對象內部定義的屬性,是不能被直接訪問的,只有當函數被調用時,變量對象(VO)被激活爲活動對象(AO)時,咱們才能訪問到其中的屬性和方法。

活動對象(activation object 簡稱 AO

函數進入執行階段時,本來不能訪問的變量對象被激活成爲一個活動對象,自此,咱們能夠訪問到其中的各類屬性。bash

其實變量對象和活動對象是一個東西,只不過處於不一樣的狀態和階段而已。數據結構

做用域鏈(scope chain

做用域 規定了如何查找變量,也就是肯定當前執行代碼對變量的訪問權限。當查找變量的時候,會先從當前上下文的變量對象中查找,若是沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫作 做用域鏈閉包

當前可執行代碼塊的調用者(this)

若是當前函數被做爲對象方法調用或使用 bind call applyAPI 進行委託調用,則將當前代碼塊的調用者信息(this value)存入當前執行上下文,不然默認爲全局對象調用。

關於 this 的建立細節,有點煩,有興趣的話能夠進入 這個章節 學習。

執行上下文數據結構模擬

若是將上述一個完整的執行上下文使用代碼形式表現出來的話,應該相似於下面這種:

executionContext:{
    [variable object | activation object]:{
        arguments,
        variables: [...],
        funcions: [...]
    },
    scope chain: variable object + all parents scopes
    thisValue: context object
}
複製代碼

ES3中的執行上下文生命週期

執行上下文的生命週期有三個階段,分別是:

  • 建立階段
  • 執行階段
  • 銷燬階段

建立階段

函數執行上下文的建立階段,發生在函數調用時且在執行函數體內的具體代碼以前,在建立階段,JS 引擎會作以下操做:

全局執行上下文
  • 執行全局代碼前,建立一個全局執行上下文

  • 對全局數據進行預處理

    • 這一階段會進行變量和函數的初始化聲明
    • var 定義的全局變量--> undefined 添加爲window屬性
    • function 聲明的全局函數 –-> 賦值(fun) 添加爲window屬性
    • this --> 賦值(window)
函數執行上下文
  • 在調用函數時,準備執行函數體以前,建立對應的函數執行上下文對象
  • 對局部數據進行預處理
    • 形參變量==》賦值(實參)--》添加爲執行上下文的屬性
    • arguments-->賦值-->(實參列表),添加爲執行上下文屬性
    • var 定義的局部變量 –-> undefined 添加爲執行上下文屬性
    • function 神明的函數 --> 賦值(fun) 添加爲執行上下文屬性
    • 構建做用域鏈(前面已經說過構建細節)
    • this --> 賦值(調用函數對象)
有沒有發現這個建立執行上下文的階段有變量和函數的初始化生命。這個操做就是 **變量聲明提高**(變量和函數聲明都會提高,可是函數提高更靠前)。
複製代碼

執行階段

執行階段中,JS 代碼開始逐條執行,在這個階段,JS 引擎開始對定義的變量賦值、開始順着做用域鏈訪問變量、若是內部有函數調用就建立一個新的執行上下文壓入執行棧並把控制權交出……

銷燬階段

通常來說當函數執行完成後,當前執行上下文(局部環境)會被彈出執行上下文棧而且銷燬,控制權被從新交給執行棧上一層的執行上下文。

注意這只是通常狀況,閉包的狀況又有所不一樣。

閉包的定義:有權訪問另外一個函數內部變量的函數。簡單說來,若是一個函數被做爲另外一個函數的返回值,並在外部被引用,那麼這個函數就被稱爲閉包。

ES3執行上下文總結

對於 ES3 中的執行上下文,咱們能夠用下面這個列表來歸納程序執行的整個過程:

  1. 函數被調用
  2. 在執行具體的函數代碼以前,建立了執行上下文
  3. 進入執行上下文的建立階段:
    1. 初始化做用域鏈
    2. 建立 arguments object 檢查上下文中的參數,初始化名稱和值並建立引用副本
    3. 掃描上下文找到全部函數聲明:
      1. 對於每一個找到的函數,用它們的原生函數名,在變量對象中建立一個屬性,該屬性裏存放的是一個指向實際內存地址的指針
      2. 若是函數名稱已經存在了,屬性的引用指針將會被覆蓋
    4. 掃描上下文找到全部var的變量聲明:
      1. 對於每一個找到的變量聲明,用它們的原生變量名,在變量對象中建立一個屬性,而且使用 undefined 來初始化
      2. 若是變量名做爲屬性在變量對象中已存在,則不作任何處理並接着掃描
    5. 肯定 this
  4. 進入執行上下文的執行階段:
    1. 在上下文中運行/解釋函數代碼,並在代碼逐行執行時分配變量值。

ES5中的執行上下文

ES5 規範又對 ES3 中執行上下文的部分概念作了調整,最主要的調整,就是去除了 ES3 中變量對象和活動對象,以 詞法環境組件( LexicalEnvironment component)變量環境組件( VariableEnvironment component) 替代。因此 ES5 的執行上下文概念上表示大概以下:

ExecutionContext = {
  ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 複製代碼

This Binding

  • 全局執行上下文中,this 的值指向全局對象,在瀏覽器中this 的值指向 window對象,而在nodejs中指向這個文件的module對象。
  • 函數執行上下文中,this 的值取決於函數的調用方式。具體有:默認綁定、隱式綁定、顯式綁定(硬綁定)、new綁定、箭頭函數,具體內容會在【this全面解析】部分詳解。

詞法環境(Lexical Environment)

詞法環境有兩個組成部分

  • 一、環境記錄:存儲變量和函數聲明的實際位置
  • 二、對外部環境的引用:能夠訪問其外部詞法環境

詞法環境有兩種類型

  • 一、全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,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>  
  }  
}
複製代碼

變量提高的緣由:在建立階段,函數聲明存儲在環境中,而變量會被設置爲 undefined(在 var 的狀況下)或保持未初始化(在 letconst 的狀況下)。因此這就是爲何能夠在聲明以前訪問 var 定義的變量(儘管是 undefined ),但若是在聲明以前訪問 letconst 定義的變量就會提示引用錯誤的緣由。這就是所謂的變量提高。

ES5 執行上下文總結

對於 ES5 中的執行上下文,咱們能夠用下面這個列表來歸納程序執行的整個過程:

  1. 程序啓動,全局上下文被建立
    1. 建立全局上下文的詞法環境
      1. 建立 對象環境記錄器 ,它用來定義出如今 全局上下文 中的變量和函數的關係(負責處理 letconst 定義的變量)
      2. 建立 外部環境引用,值爲 null
    2. 建立全局上下文的變量環境
      1. 建立 對象環境記錄器,它持有 變量聲明語句 在執行上下文中建立的綁定關係(負責處理 var 定義的變量,初始值爲 undefined 形成聲明提高)
      2. 建立 外部環境引用,值爲 null
    3. 肯定 this 值爲全局對象(以瀏覽器爲例,就是 window
  2. 函數被調用,函數上下文被建立
    1. 建立函數上下文的詞法環境
      1. 建立 聲明式環境記錄器 ,存儲變量、函數和參數,它包含了一個傳遞給函數的 arguments 對象(此對象存儲索引和參數的映射)和傳遞給函數的參數的 length。(負責處理 letconst 定義的變量)
      2. 建立 外部環境引用,值爲全局對象,或者爲父級詞法環境(做用域)
    2. 建立函數上下文的變量環境
      1. 建立 聲明式環境記錄器 ,存儲變量、函數和參數,它包含了一個傳遞給函數的 arguments 對象(此對象存儲索引和參數的映射)和傳遞給函數的參數的 length。(負責處理 var 定義的變量,初始值爲 undefined 形成聲明提高)
      2. 建立 外部環境引用,值爲全局對象,或者爲父級詞法環境(做用域)
    3. 肯定 this
  3. 進入函數執行上下文的執行階段:
    1. 在上下文中運行/解釋函數代碼,並在代碼逐行執行時分配變量值。

執行棧

執行棧,也就是在其它編程語言中所說的「調用棧」,是一種擁有 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');

複製代碼

圖片

上述代碼的執行上下文棧。

當上述代碼在瀏覽器加載時,JavaScript 引擎建立了一個全局執行上下文並把它壓入當前執行棧。當遇到 first() 函數調用時,JavaScript 引擎爲該函數建立一個新的執行上下文並把它壓入當前執行棧的頂部。

當從 first() 函數內部調用 second() 函數時,JavaScript 引擎爲 second() 函數建立了一個新的執行上下文並把它壓入當前執行棧的頂部。當 second() 函數執行完畢,它的執行上下文會從當前棧彈出,而且控制流程到達下一個執行上下文,即 first() 函數的執行上下文。

first() 執行完畢,它的執行上下文從棧彈出,控制流程到達全局執行上下文。一旦全部代碼執行完畢,JavaScript 引擎從當前棧中移除全局執行上下文。

結論

  1. 執行上下文建立階段分爲綁定this,建立詞法環境,變量環境三步,二者區別在於詞法環境存放函數聲明與const let聲明的變量,而變量環境只存儲var聲明的變量。
  2. 詞法環境主要由環境記錄與外部環境引入記錄兩個部分組成,全局上下文與函數上下文的外部環境引入記錄不同,全局爲null,函數爲全局環境或者其它函數環境。環境記錄也不同,全局叫對象環境記錄,函數叫聲明性環境記錄。
  3. 你應該明白爲何會存在變量提高,函數提高,而let const沒有。
  4. ES3以前的變量對象與活動對象的概念在ES5以後由詞法環境,變量環境來解釋,二者概念不衝突,後者理解更爲通俗易懂。不得不說相關文章也是看的我心累,也但願對有緣的你有所幫助,那麼到這裏,本文結束。

參考

JavaScript執行上下文和執行棧

JavaScript深刻之執行上下文棧

相關文章
相關標籤/搜索