JavaScript 之深刻理解執行上下文

在 JavaScript 中,執行上下文是一個基本的概念,但其中又包含了變量對象、做用域鏈、this 指向等更深刻的內容,深刻理解執行上下文以及其中的內容,對咱們之後理解 JavaScript 中其它更深刻的知識點(函數/變量提高、閉包等)會有很大的幫助。前端

執行上下文(Execution Context)

執行上下文能夠理解爲當前代碼的運行環境。在 JavaScript 中,運行環境主要包含了全局環境函數環境es6

在 JavaScript 代碼運行過程當中,最早進入的是全局環境,而在函數被調用時則進入相應的函數環境。全局環境和函數環境所對應的執行上下文咱們分別稱爲全局上下文函數上下文express

在一個 JavaScript 文件中,常常會有多個函數被調用,也就是說在 JavaScript 代碼運行過程當中極可能會產生多個執行上下文,那麼如何去管理這多個執行上下文呢?數組

執行上下文是以棧(一種 LIFO 的數據結構)的方式被存放起來的,咱們稱之爲執行上下文棧(Execution Context Stack)瀏覽器

在 JavaScript 代碼開始執行時,首先進入全局環境,此時全局上下文被建立併入棧,以後當調用函數時則進入相應的函數環境,此時相應函數上下文被建立併入棧,當處於棧頂的執行上下文代碼執行完畢後,則會將其出棧。bash

因此在執行上下文棧中,棧底永遠是全局上下文,而棧頂則是當前正在執行的函數上下文。數據結構

文字表達既枯燥又難以理解,讓咱們來看一個簡單的栗子吧~閉包

function fn2() {
  console.log('fn2')
}
function fn1() {
  console.log('fn1')
  fn2();
}
fn1();
複製代碼

運行上述代碼,能夠獲得相應的輸出,那麼上述代碼在執行過程當中執行上下文棧的行爲是怎樣的呢?函數

/* 僞代碼 以數組來表示執行上下文棧 ECStack=[] */
// 代碼執行時最早進入全局環境,全局上下文被建立併入棧
ECStack.push(global_EC);
// fn1 被調用,fn1 函數上下文被建立併入棧
ECStack.push(fn1_EC);
// fn1 中調用 fn2,fn2 函數上下文被建立併入棧
ECStack.push(fn2_EC);
// fn2 執行完畢,fn2 函數上下文出棧
ECStack.pop();
// fn1 執行完畢,fn1 函數上下文出棧
ECStack.pop();
// 代碼執行完畢,全局上下文出棧
ECStack.pop();
複製代碼

以一個更形象的圖來講明上述的流程post

執行上下文棧 ECStack

在一個執行上下文中,最重要的三個屬性分別是變量對象(Variable Object)、**做用域鏈(Scope Chain)**和 this 指向

咱們能夠採用以下方式表示

EC = {
  VO,
  SC,
  this
}
複製代碼

一個執行上下文的生命週期分爲建立執行階段。建立階段主要工做是生成變量對象創建做用域鏈肯定 this 指向。而執行階段主要工做是變量賦值以及執行其它代碼等。

變量對象(Variable Object)

咱們已經知道,在執行上下文的建立階段會生成變量對象,生成變量對象主要有如下三個過程:

  1. 檢索當前上下文中的參數。該過程生成 Arguments 對象,並創建以形參變量名爲屬性名,形參變量值爲屬性值的屬性;
  2. 檢索當前上下文中的函數聲明。該過程創建以函數名爲屬性名,函數所在內存地址引用爲屬性值的屬性;
  3. 檢索當前上下文中的變量聲明。該過程創建以變量名爲屬性名,undefined 爲屬性值的屬性(若是變量名跟已聲明的形參變量名或函數名相同,則該變量聲明不會干擾已經存在的這類屬性)。

咱們能夠經過如下僞代碼來表示變量對象

VO = {
  Arguments: {}, 
  ParamVariable: 具體值,  //形參變量
  Function: <function reference>, Variable:undefined } 複製代碼

當執行上下文進入執行階段後,變量對象會變爲活動對象(Active Object)。此時原先聲明的變量會被賦值。

變量對象和活動對象都是指同一個對象,只是處於執行上下文的不一樣階段

咱們能夠經過如下僞代碼來表示活動對象

AO = {
  Arguments: {},
  ParamVariable: 具體值,  //形參變量
  Function: <function reference>, Variable:具體值 } 複製代碼

一樣的,讓咱們以實際栗子來理解在代碼執行過程當中某執行上下文中變量對象的變化狀況~

function fn1(a) {
  var b = 1;
  function fn2() {}
  var c = function () {};
}
fn1(0);
複製代碼

當 fn1 函數被調用時,fn1 執行上下文被建立(建立階段)併入棧,其變量對象以下所示

fn1_EC = {
  VO = {
    Arguments: {
      '0': 0,
      length: 1
    },
    a: 0,
    b: undefined,
    fn2: <function fn2 reference>, c:undefined } } 複製代碼

而在 fn1 函數代碼的執行過程當中(執行階段),變量對象變爲活動對象,原先聲明的變量會被賦值,其活動對象以下所示

fn1_EC = {
  AO = {
    Arguments: {
      '0': 0,
      length: 1
    },
    a: 0,
    b: 1,
    fn2: <function fn2 reference>,
    c:<function express c reference>,
  }
}
複製代碼

對於全局上下文來講,因爲其不會有參數傳遞,因此在生成變量對象的過程當中只有檢索當前上下文中的函數聲明和檢索當前上下文中的變量聲明兩個步驟。

在瀏覽器環境中,全局上下文中的變量對象(全局對象)即咱們熟悉的 window 對象,經過該對象可使用其預約義的變量和函數,在全局環境中所聲明的變量和函數,也會成爲全局對象的屬性。

弄明白了變量對象的生成過程後,咱們就可以更深刻地理解函數提高以及變量提高的內在機制了。

console.log(a) // undefined
fn(0); // fn
var a = 0;
function fn() {
  console.log('fn')
}
複製代碼

上述代碼中,在全局上下文的建立階段,會檢索上下文中的函數聲明以及變量聲明,函數會被賦值具體的引用地址而變量會被賦值爲 undefined。

因此上述代碼實際上的運行過程以下

function fn() {
  console.log('fn')
}
var a = undefined;
console.log(a) // undefined
fn(0); // fn
a = 0;
複製代碼

因此,這就是咱們常常提到的函數提高以及變量提高的內在機制。

做用域鏈(Scope Chain)

做用域鏈是指由當前上下文和上層上下文的一系列變量對象組成的層級鏈。它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

咱們已經知道,執行上下文分爲建立和執行兩個階段,在執行上下文的執行階段,當須要查找某個變量或函數時,會在當前上下文的變量對象(活動對象)中進行查找,如果沒有找到,則會沿着上層上下文的變量對象進行查找,直到全局上下文中的變量對象(全局對象)。

那麼當前上下文是如何有序地去查找它所須要的變量或函數的呢?

答案就是依靠當前上下文中的做用域鏈,其包含了當前上下文和上層上下文中的變量對象,以便其一層一層地去查找其所須要的變量和函數。

執行上下文中的做用域鏈又是怎麼創建的呢?

咱們都知道,JavaScript 中主要包含了全局做用域和函數做用域,而函數做用域是在函數被聲明的時候肯定的

每個函數都會包含一個 [[scope]] 內部屬性,在函數被聲明的時候,該函數的 [[scope]] 屬性會保存其上層上下文的變量對象,造成包含上層上下文變量對象的層級鏈。[[scope]] 屬性的值是在函數被聲明的時候肯定的

當函數被調用的時候,其執行上下文會被建立併入棧。在建立階段生成其變量對象後,會將該變量對象添加到做用域鏈的頂端並將 [[scope]] 添加進該做用域鏈中。而在執行階段,變量對象會變爲活動對象,其相應屬性會被賦值。

因此,做用域鏈是由當前上下文變量對象及上層上下文變量對象組成的

SC = AO + [[scope]]
複製代碼

讓咱們來看個栗子~

var a = 1;
function fn1() {
  var b = 1;
  function fn2() {
    var c = 1;
  }
  fn2();
}
fn1();
複製代碼

在 fn1 函數上下文中,fn2 函數被聲明,因此

fn2.[[scope]]=[fn1_EC.VO, globalObj]
複製代碼

當 fn2 被調用的時候,其執行上下文被建立併入棧,此時會將生成的變量對象添加進做用域鏈的頂端,而且將 [[scope]] 添加進做用域鏈

fn2_EC.SC=[fn2_EC.VO].concat(fn2.[[scope]])
=>
fn2_EC.SC=[fn2_EC.VO, fn1_EC.VO, globalObj]
複製代碼

this 指向

**this 的指向,是在函數被調用的時候肯定的。**也就是執行上下文被建立時肯定的。

關於 this 的指向,其實最主要的是三種場景,分別是全局上下文中 this函數中 this構造函數中 this

全局上下文中 this

在全局上下文中,this 指代全局對象。

// 在瀏覽器環境中,全局對象是 window 對象:
console.log(this === window); // true
a = 1;
this.b = 2;
console.log(window.a); // 1
console.log(window.b); // 2
console.log(b); // 2
複製代碼

函數中 this

函數中的 this 指向是怎樣一種狀況呢?

若是被調用的函數,被某一個對象所擁有,那麼其內部的 this 指向該對象;若是該函數被獨立調用,那麼其內部的 this 指向 undefined(非嚴格模式下指向 window)。

舉個栗子~

var a = 1;
function fn() {
  console.log(this.a)
}
var obj = {
  a: 2,
  fn: fn
}
obj.fn(); // 2
fn(); // 1
複製代碼

上述代碼中 fn 函數都是輸出 this.a,根據上述的結論,obj.fn() 因爲其是被 obj 對象所擁有,因此 this 指向 obj 對象;而 fn 是被獨立調用,在非嚴格模式下 this 指向 window。

構造函數中 this

要清楚構造函數中 this 的指向,則必須先了解經過 new 操做符調用構造函數時所經歷的階段。

經過 new 操做符調用構造函數時所經歷的階段以下:

  1. 建立一個新對象;
  2. 將構造函數的 this 指向這個新對象;
  3. 執行構造函數內部代碼;
  4. 返回這個新對象。

因此從上述流程可知,對於構造函數來講,其內部 this 指向新建立的對象實例

function Person(name, age) {
  this.name = name;
  this.age = age;
}
var ttsy = new Person('ttsy', 24);
console.log(ttsy.name);  // ttsy
console.log(ttsy.age);  // 24
複製代碼

須要注意的是,在 ES6 中箭頭函數中,this 是在函數聲明的時候肯定的,具體可看 es6.ruanyifeng.com/#docs/funct…

一個完整的栗子

接下來,讓咱們來完整地 look 一下程序運行過程當中執行上下文及其內部屬性的變化狀況。

function fn1() {
  var a = 1;
  function fn2(b) {
    var c = 3
  }
  fn2(2)
}
fn1();
複製代碼

上述代碼在執行過程當中,執行上下文棧的變化過程以下

/* 僞代碼 以數組來表示執行上下文棧 ECStack=[] */
// 代碼執行時最早進入全局環境,全局上下文被建立併入棧
ECStack.push(global_EC);
// fn1 被調用,fn1 函數上下文被建立併入棧
ECStack.push(fn1_EC);
// fn1 中調用 fn2,fn2 函數上下文被建立併入棧
ECStack.push(fn2_EC);
// fn2 執行完畢,fn2 函數上下文出棧
ECStack.pop();
// fn1 執行完畢,fn1 函數上下文出棧
ECStack.pop();
// 代碼執行完畢,全局上下文出棧
ECStack.pop();
複製代碼

首先進入全局環境,全局上下文被建立併入棧

全局上下文以下

global_EC = {
  VO: globalObj,
  SC: [globalObj],
  this: globalObj,
}
複製代碼

接着 fn1 被調用,fn1 函數上下文被建立併入棧

在 fn1 函數上下文被建立以前,會有一個函數聲明過程,這個過程發生在全局上下文建立階段,在這個過程當中,fn1.[[scope]] 會保存其上層做用域的變量對象。

在 fn1 函數上下文建立階段,其執行上下文以下

fn1_EC = {
  VO: {
    Arguments: {
      length: 0
    },
    fn2: <function fn2 reference>, a:undefined }, SC:[fn1_EC.VO, globalObj], this:null } 複製代碼

在 fn1 函數上下文執行階段,其執行上下文以下

fn1_EC = {
  VO: {
    Arguments: {
      length: 0
    },
    fn2: <function fn2 reference>, a:1 }, SC:[fn1_EC.VO, globalObj], this:globalObj } 複製代碼

而後在 fn1 中調用 fn2,fn2 函數上下文被建立併入棧

在 fn2 函數上下文建立階段,其執行上下文以下

fn2_EC = {
  VO: {
    Arguments: {
      '0': 2,
      length: 0
    },
    b: 2,
    c: undefined
  },
  SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
  this: null
}
複製代碼

在 fn2 函數上下文執行階段,其執行上下文以下

fn2_EC = {
  VO: {
    Arguments: {
      '0': 2,
      length: 0
    },
    b: 2,
    c: 3
  },
  SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
  this: globalObj
}
複製代碼

最後是各個上下文出棧

在各個上下文出棧後,其對應的變量對象會被 JavaScript 中的自動垃圾收集機制回收。

而咱們常常說閉包可以訪問其所在環境的變量,實際上是由於閉包可以阻止上述變量對象被回收的過程。

深刻地理解了執行上下文的內容後,對於咱們理解閉包也會有很大的幫助,關於閉包我寫過一篇 《 JavaScript 閉包詳解 》,感興趣的童鞋也能夠繼續閱讀。


公衆號不定時分享我的在前端方面的學習經驗,歡迎關注。

相關文章
相關標籤/搜索