理解 JavaScript 中的執行上下文

一塊兒看看 JavaScript 程序內部是如何執行的。javascript

本文翻譯自 blog.bitsrc.io/understandi…,做者 Sukhjinder Arora,有部分刪改。前端

若是你想成爲一個合格的 JavaScript 開發者,你必須知道它的內部是如何執行的。掌握 JavaScript 執行上下文和執行棧對理解變量提高、做用域和閉包很是重要。java

理解執行上下文和執行棧將使你成爲一個更加優秀的 JavaScript 開發者。編程

執行上下文是什麼?

執行上下文是一個 JavaScript 代碼運行的環境。任何 JavaScript 代碼執行的時候都是處於一個執行上下文中。windows

執行上下文的類型

JavaScript 中一共有三種執行上下文。數組

  • 全局執行上下文(Global) -- 它是默認的基本執行上下文。代碼要麼在全局執行上下文要麼在函數執行上下文。它有兩個特徵:它會建立一個全局對象(在瀏覽器中就是 window)而且會把 this 設置爲全局對象 windows。在一個程序中只會有一個全局執行上下文。
  • 函數執行上下文 -- 當函數執行的時候,一個新的函數執行上下文就會建立。每一個函數都有本身的執行上下文,當函數執行的時候上下文會被建立。函數執行上下文能夠建立任意多個,每一個執行上下文被建立的時候會經歷若干步驟,接下來將會討論。
  • eval 函數執行上下文 -- 在 eval 函數中執行的代碼也會有本身的自行上下文,但因爲 eval 已經不經常使用了,因此不作討論。

執行棧

執行棧(執行上下文棧),在其餘編程語言中也叫調用棧,是一個後進先出的結構。它用來存儲代碼執行過程當中建立的全部執行上下文。瀏覽器

當 JavaScript 引擎執行你的代碼時,它會建立一個全局執行上下文而且將它推入當前的執行棧。當執行碰到函數調用的時候,它會爲這個函數建立執行上下文並把這個執行上下文推入執行棧頂部。閉包

引擎執行處於棧頂的上下文對應的函數。當函數執行完畢,它的上下文就會從棧頂彈出,引擎接着繼續執行新處於頂部的上下文對應的函數。編程語言

看看下面的例子:ide

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() 執行時,引擎給這個函數建立一個新的執行上下文,而後把它推入執行棧頂部。

second()first() 函數內部執行時,引擎會給 second 建立上下文並把它推入執行棧頂,當 second 函數執行完畢,它的執行上下文就會從執行棧頂彈出,指針會指向它下面的上下文,也就是 first 函數的上下文。

first 函數執行完畢,它的執行棧也會從棧頂彈出,指針就指向了全局執行上下文。當全部的代碼執行完畢,引擎會把全局執行上下文也從執行棧中移出。

執行上下文是如何建立的

從上面的過程,咱們已經瞭解了 JavaScript 引擎是如何管理執行上下文的,接下來咱們看看引擎是如何建立執行上下文的。

執行上下文會經歷兩個階段:1 建立階段;2 執行階段。

建立階段

執行上下文在建立階段就會被建立。建立階段作下面兩件事:

  1. 建立詞法環境(LexicalEnvironment)
  2. 建立變量環境(VariableEnvironment)

因此從概念上說,執行上下文能夠用下面的方式表示:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}
複製代碼

詞法環境(Lexical Environment)

ES6 官方文檔是這樣定義詞法環境的

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.

簡單來講,詞法環境是一種表示標識符和變量的映射關係的環境。在詞法環境中,標識符指向變量或者函數,變量是指對象(包括函數對象和數組對象)或者原始值。

舉個例子,看看下面的代碼

var a = 20;
var b = 40;
function foo() {
  console.log('bar');
}
複製代碼

上面代碼的詞法環境以下

lexicalEnvironment = {
  a: 20,
  b: 40,
  foo: <ref. to foo function>
}
複製代碼

每一個詞法環境由三個部分組成:

  1. 詞法環境內部的環境記錄(Environment Record);
  2. 一個指向外層詞法環境的可空引用(Reference to the outer environment);
  3. this
環境記錄(Environment Record)

Environment Record 是在詞法環境中存儲變量和函數的地方。

Environment Record 有下面兩種:

  • Declarative environment record -- As its name suggests stores variable and function declarations. The lexical environment for function code contains a declarative environment record.
  • Object environment record -- The lexical environment for global code contains a objective environment record. Apart from variable and function declarations, the object environment record also stores a global binding object (window object in browsers). So for each of binding object’s property (in case of browsers, it contains properties and methods provided by browser to the window object), a new entry is created in the record.

上面是原文,簡單解釋下:

  • Declarative environment record -- 用來放變量或者函數聲明,函數中的詞法環境都是這種;
  • Object environment record -- 指向全局對象 window(在瀏覽器中),全局詞法環境是這種;

注意:對於函數,環境記錄也包括一個 arguments 對象。arguments 是一個類數組對象,它包含索引和參數值的映射。看看下面的例子:

function foo(a, b) {
  var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
複製代碼
outer 是什麼

outer 表示一個做用域指向的外層詞法環境。在查找變量時,若是在當前的詞法環境裏面沒有找到變量,那就經過 outer 找到外層的詞法環境,而後再在外層的詞法環境裏面查找變量,若是尚未找到,則會繼續往外層找,一直找到全局做用域。

this 怎麼肯定

在全局執行上下文中,this 指向全局對象 window(在瀏覽器中)。

在函數執行上下文中,this 取決於函數是如何被調用的。這是咱們常常弄混的一點。若是是經過對象調用的函數,那 this 指向這個對象。不然 this 將會指向全局對象(在瀏覽器中是 window)或者 undefined(嚴格模式下) 。 看下面的例子:

const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge();
// 'this' 指向 'person', 由於 'calcAge' 是經過 `person` 對象調用的。
const calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局對象,由於函數不是經過對象引用的方式調用的。
複製代碼

詞法做用域用僞代碼表示是這樣的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}
複製代碼

變量環境(Variable Environment)

變量環境也是一個詞法環境,它和詞法環境長得同樣。區別在於,在 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);
複製代碼

當上面的代碼執行的時候,JavaScript 引擎會建立一個全局執行上下文來執行全局的代碼。因此在建立階段(creation phase)全局執行上下文是像這樣的:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}
複製代碼

在執行階段(execution phase),會進行變量賦值。全局執行上下文將會變成下面這樣:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}
複製代碼

當碰到要執行 multiply(20, 30) 時,一個新的函數執行上下文會建立。在建立階段(creation phase)函數執行上下文會像下面這樣:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2}, // 函數的參數也在詞法環境中
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}
複製代碼

在執行階段(execution phase)會進行變量賦值。賦值以後的函數執行上下文以下:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}
複製代碼

函數執行完成時,返回的值將會賦值給 c,全局詞法環境將會更新,而後全部代碼執行完畢,程序結束。

你可能注意到了 letconst 聲明的變量在建立階段(creation phase) 和它的值沒有任何關聯,可是 var 聲明的變量被賦予了 undefined

這是由於在建立階段 JavaScript 引擎會掃描到變量和函數聲明。用 var 聲明的變量被初始化爲 undefined,用 let const 聲明的變量將不會被初始化。後者將會造成暫時性死區,提早使用它們將會報錯。

暫時性死區

這就是變量提高。

注意,在執行階段,若是引擎發現 let 聲明的變量並無被賦值,引擎將會把它賦值爲 undefined

最後

感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術。。

公衆號
相關文章
相關標籤/搜索