首頁技術文章正文

理解 JavaScript 中的執(zhí)行上下文和執(zhí)行棧

更新時間:2018-10-31 來源:黑馬程序員 瀏覽量:

  什么是執(zhí)行上下文?簡而言之,執(zhí)行上下文是評估和執(zhí)行 JavaScript 代碼的環(huán)境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執(zhí)行上下文中運行。

  執(zhí)行上下文的類型JavaScript 中有三種執(zhí)行上下文類型。

  全局執(zhí)行上下文 — 這是默認或者說基礎的上下文,任何不在函數內部的代碼都在全局上下文中。它會執(zhí)行兩件事:創(chuàng)建一個全局的 window 對象(瀏覽器的情況下),并且設置 this 的值等于這個全局對象。一個程序中只會有一個全局執(zhí)行上下文。

  函數執(zhí)行上下文 — 每當一個函數被調用時, 都會為該函數創(chuàng)建一個新的上下文。每個函數都有它自己的執(zhí)行上下文,不過是在函數被調用時創(chuàng)建的。函數上下文可以有任意多個。每當一個新的執(zhí)行上下文被創(chuàng)建,它會按定義的順序(將在后文討論)執(zhí)行一系列步驟。

  Eval 函數執(zhí)行上下文 — 執(zhí)行在 eval 函數內部的代碼也會有它屬于自己的執(zhí)行上下文,但由于 JavaScript 開發(fā)者并不經常使用 eval,所以在這里我不會討論它。

  執(zhí)行棧執(zhí)行棧,也就是在其它編程語言中所說的“調用?!?,是一種擁有 LIFO(后進先出)數據結構的棧,被用來存儲代碼運行時創(chuàng)建的所有執(zhí)行上下文。

  當 JavaScript 引擎第一次遇到你的腳本時,它會創(chuàng)建一個全局的執(zhí)行上下文并且壓入當前執(zhí)行棧。每當引擎遇到一個函數調用,它會為該函數創(chuàng)建一個新的執(zhí)行上下文并壓入棧的頂部。

  引擎會執(zhí)行那些執(zhí)行上下文位于棧頂的函數。當該函數執(zhí)行結束時,執(zhí)行上下文從棧中彈出,控制流程到達當前棧中的下一個上下文。

  讓我們通過下面的代碼示例來理解:

  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 引擎創(chuàng)建了一個全局執(zhí)行上下文并把它壓入當前執(zhí)行棧。當遇到 first() 函數調用時,JavaScript 引擎為該函數創(chuàng)建一個新的執(zhí)行上下文并把它壓入當前執(zhí)行棧的頂部。

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

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

  怎么創(chuàng)建執(zhí)行上下文?到現在,我們已經看過 JavaScript 怎樣管理執(zhí)行上下文了,現在讓我們了解 JavaScript 引擎是怎樣創(chuàng)建執(zhí)行上下文的。

  創(chuàng)建執(zhí)行上下文有兩個階段:1) 創(chuàng)建階段 和 2) 執(zhí)行階段。

  The Creation Phase在 JavaScript 代碼執(zhí)行前,執(zhí)行上下文將經歷創(chuàng)建階段。在創(chuàng)建階段會發(fā)生三件事:

  this 值的決定,即我們所熟知的 This 綁定。

  創(chuàng)建詞法環(huán)境組件。

  創(chuàng)建變量環(huán)境組件。

  所以執(zhí)行上下文在概念上表示如下:

  ExecutionContext = {

  ThisBinding =,

  LexicalEnvironment = { ... },

  VariableEnvironment = { ... },

  }

  This 綁定:在全局執(zhí)行上下文中,this 的值指向全局對象。(在瀏覽器中,this引用 Window 對象)。

  在函數執(zhí)行上下文中,this 的值取決于該函數是如何被調用的。如果它被一個引用對象調用,那么 this 會被設置成那個對象,否則 this 的值被設置為全局對象或者 undefined(在嚴格模式下)。例如:

  let foo = {

  baz: function() {

  console.log(this);

  }

  }

  foo.baz(); // 'this' 引用 'foo', 因為 'baz' 被

  // 對象 'foo' 調用

  let bar = foo.baz;

  bar(); // 'this' 指向全局 window 對象,因為

  // 沒有指定引用對象

  詞法環(huán)境官方的 ES6 文檔把詞法環(huán)境定義為

  詞法環(huán)境是一種規(guī)范類型,基于 ECMAScript 代碼的詞法嵌套結構來定義標識符和具體變量和函數的關聯(lián)。一個詞法環(huán)境由環(huán)境記錄器和一個可能的引用外部詞法環(huán)境的空值組成。

  簡單來說詞法環(huán)境是一種持有標識符—變量映射的結構。(這里的標識符指的是變量/函數的名字,而變量是對實際對象[包含函數類型對象]或原始數據的引用)。

  現在,在詞法環(huán)境的內部有兩個組件:(1) 環(huán)境記錄器和 (2) 一個外部環(huán)境的引用。

  環(huán)境記錄器是存儲變量和函數聲明的實際位置。

  外部環(huán)境的引用意味著它可以訪問其父級詞法環(huán)境(作用域)。

  詞法環(huán)境有兩種類型:

  全局環(huán)境(在全局執(zhí)行上下文中)是沒有外部環(huán)境引用的詞法環(huán)境。全局環(huán)境的外部環(huán)境引用是 null。它擁有內建的 Object/Array/等、在環(huán)境記錄器內的原型函數(關聯(lián)全局對象,比如 window 對象)還有任何用戶定義的全局變量,并且 this的值指向全局對象。

  在函數環(huán)境中,函數內部用戶定義的變量存儲在環(huán)境記錄器中。并且引用的外部環(huán)境可能是全局環(huán)境,或者任何包含此內部函數的外部函數。

  環(huán)境記錄器也有兩種類型(如上!):

  聲明式環(huán)境記錄器存儲變量、函數和參數。

  對象環(huán)境記錄器用來定義出現在全局上下文中的變量和函數的關系。

  簡而言之,

  在全局環(huán)境中,環(huán)境記錄器是對象環(huán)境記錄器。

  在函數環(huán)境中,環(huán)境記錄器是聲明式環(huán)境記錄器。

  注意 — 對于函數環(huán)境,聲明式環(huán)境記錄器還包含了一個傳遞給函數的 arguments 對象(此對象存儲索引和參數的映射)和傳遞給函數的參數的 length。

  抽象地講,詞法環(huán)境在偽代碼中看起來像這樣:

  GlobalExectionContext = {

  LexicalEnvironment: {

  EnvironmentRecord: {

  Type: "Object",

  // 在這里綁定標識符

  }

  outer:

  }

  }

  FunctionExectionContext = {

  LexicalEnvironment: {

  EnvironmentRecord: {

  Type: "Declarative",

  // 在這里綁定標識符

  }

  outer:

  }

  }

  變量環(huán)境:它同樣是一個詞法環(huán)境,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關系。

  如上所述,變量環(huán)境也是一個詞法環(huán)境,所以它有著上面定義的詞法環(huán)境的所有屬性。

  在 ES6 中,詞法環(huán)境組件和變量環(huán)境的一個不同就是前者被用來存儲函數聲明和變量(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);

  執(zhí)行上下文看起來像這樣:

  GlobalExectionContext = {

  ThisBinding:,

  LexicalEnvironment: {

  EnvironmentRecord: {

  Type: "Object",

  // 在這里綁定標識符

  a: < uninitialized >,

  b: < uninitialized >,

  multiply: < func >

  }

  outer:

  },

  VariableEnvironment: {

  EnvironmentRecord: {

  Type: "Object",

  // 在這里綁定標識符

  c: undefined,

  }

  outer:

  }

  }

  FunctionExectionContext = {

  ThisBinding:,

  LexicalEnvironment: {

  EnvironmentRecord: {

  Type: "Declarative",

  // 在這里綁定標識符

  Arguments: {0: 20, 1: 30, length: 2},

  },

  outer:

  },

  VariableEnvironment: {

  EnvironmentRecord: {

  Type: "Declarative",

  // 在這里綁定標識符

  g: undefined

  },

  outer:

  }

  }

  注意 — 只有遇到調用函數 multiply 時,函數執(zhí)行上下文才會被創(chuàng)建。

  可能你已經注意到 let 和 const 定義的變量并沒有關聯(lián)任何值,但 var 定義的變量被設成了 undefined。

  這是因為在創(chuàng)建階段時,引擎檢查代碼找出變量和函數聲明,雖然函數聲明完全存儲在環(huán)境中,但是變量最初設置為 undefined(var 情況下),或者未初始化(let 和 const 情況下)。

  這就是為什么你可以在聲明之前訪問 var 定義的變量(雖然是 undefined),但是在聲明之前訪問 let 和 const 的變量會得到一個引用錯誤。

  這就是我們說的變量聲明提升。

  執(zhí)行階段這是整篇文章中最簡單的部分。在此階段,完成對所有這些變量的分配,最后執(zhí)行代碼。

  注意 — 在執(zhí)行階段,如果 JavaScript 引擎不能在源碼中聲明的實際位置找到 let 變量的值,它會被賦值為 undefined。

  結論我們已經討論過 JavaScript 程序內部是如何執(zhí)行的。雖然要成為一名卓越的 JavaScript 開發(fā)者并不需要學會全部這些概念,但是如果對上面概念能有不錯的理解將有助于你更輕松,更深入地理解其他概念,如變量聲明提升,作用域和閉包。

  就是這樣,如果你發(fā)現這篇文章有用,請點擊



作者:黑馬程序員前端與移動開發(fā)培訓學院
首發(fā):http://web.itheima.com

分享到:
在線咨詢 我要報名
和我們在線交談!