JavaScript 之深入理解執(zhí)行上下文

在 JavaScript 中,執(zhí)行上下文是一個基本的概念,但其中又包含了變量對象、作用域鏈、this 指向等更深入的內(nèi)容,深入理解執(zhí)行上下文以及其中的內(nèi)容,對我們以后理解 JavaScript 中其它更深入的知識點(函數(shù)/變量提升、閉包等)會有很大的幫助。

執(zhí)行上下文(Execution Context)

執(zhí)行上下文可以理解為當(dāng)前代碼的運行環(huán)境。在 JavaScript 中,運行環(huán)境主要包含了全局環(huán)境函數(shù)環(huán)境。

在 JavaScript 代碼運行過程中,最先進入的是全局環(huán)境,而在函數(shù)被調(diào)用時則進入相應(yīng)的函數(shù)環(huán)境。全局環(huán)境和函數(shù)環(huán)境所對應(yīng)的執(zhí)行上下文我們分別稱為全局上下文函數(shù)上下文。

在一個 JavaScript 文件中,經(jīng)常會有多個函數(shù)被調(diào)用,也就是說在 JavaScript 代碼運行過程中很可能會產(chǎn)生多個執(zhí)行上下文,那么如何去管理這多個執(zhí)行上下文呢?

執(zhí)行上下文是以棧(一種 LIFO 的數(shù)據(jù)結(jié)構(gòu))的方式被存放起來的,我們稱之為執(zhí)行上下文棧(Execution Context Stack)

在 JavaScript 代碼開始執(zhí)行時,首先進入全局環(huán)境,此時全局上下文被創(chuàng)建并入棧,之后當(dāng)調(diào)用函數(shù)時則進入相應(yīng)的函數(shù)環(huán)境,此時相應(yīng)函數(shù)上下文被創(chuàng)建并入棧,當(dāng)處于棧頂?shù)膱?zhí)行上下文代碼執(zhí)行完畢后,則會將其出棧。

所以在執(zhí)行上下文棧中,棧底永遠(yuǎn)是全局上下文,而棧頂則是當(dāng)前正在執(zhí)行的函數(shù)上下文。

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

function fn2() {
  console.log('fn2')
}
function fn1() {
  console.log('fn1')
  fn2();
}
fn1();

運行上述代碼,可以得到相應(yīng)的輸出,那么上述代碼在執(zhí)行過程中執(zhí)行上下文棧的行為是怎樣的呢?

/* 偽代碼 以數(shù)組來表示執(zhí)行上下文棧 ECStack=[] */
// 代碼執(zhí)行時最先進入全局環(huán)境,全局上下文被創(chuàng)建并入棧
ECStack.push(global_EC);
// fn1 被調(diào)用,fn1 函數(shù)上下文被創(chuàng)建并入棧
ECStack.push(fn1_EC);
// fn1 中調(diào)用 fn2,fn2 函數(shù)上下文被創(chuàng)建并入棧
ECStack.push(fn2_EC);
// fn2 執(zhí)行完畢,fn2 函數(shù)上下文出棧
ECStack.pop();
// fn1 執(zhí)行完畢,fn1 函數(shù)上下文出棧
ECStack.pop();
// 代碼執(zhí)行完畢,全局上下文出棧
ECStack.pop();

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

執(zhí)行上下文棧 ECStack

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

我們可以采用如下方式表示

EC = {
  VO,
  SC,
  this
}

一個執(zhí)行上下文的生命周期分為創(chuàng)建執(zhí)行階段。創(chuàng)建階段主要工作是生成變量對象、建立作用域鏈確定 this 指向。而執(zhí)行階段主要工作是變量賦值以及執(zhí)行其它代碼等。

變量對象(Variable Object)

我們已經(jīng)知道,在執(zhí)行上下文的創(chuàng)建階段會生成變量對象,生成變量對象主要有以下三個過程:

  1. 檢索當(dāng)前上下文中的參數(shù)。該過程生成 Arguments 對象,并建立以形參變量名為屬性名,形參變量值為屬性值的屬性;
  2. 檢索當(dāng)前上下文中的函數(shù)聲明。該過程建立以函數(shù)名為屬性名,函數(shù)所在內(nèi)存地址引用為屬性值的屬性;
  3. 檢索當(dāng)前上下文中的變量聲明。該過程建立以變量名為屬性名,undefined 為屬性值的屬性(如果變量名跟已聲明的形參變量名或函數(shù)名相同,則該變量聲明不會干擾已經(jīng)存在的這類屬性)。

我們可以通過以下偽代碼來表示變量對象

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

當(dāng)執(zhí)行上下文進入執(zhí)行階段后,變量對象會變?yōu)?strong>活動對象(Active Object)。此時原先聲明的變量會被賦值。

變量對象和活動對象都是指同一個對象,只是處于執(zhí)行上下文的不同階段。

我們可以通過以下偽代碼來表示活動對象

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

同樣的,讓我們以實際栗子來理解在代碼執(zhí)行過程中某執(zhí)行上下文中變量對象的變化情況~

function fn1(a) {
  var b = 1;
  function fn2() {}
  var c = function () {};
}
fn1(0);

當(dāng) fn1 函數(shù)被調(diào)用時,fn1 執(zhí)行上下文被創(chuàng)建(創(chuàng)建階段)并入棧,其變量對象如下所示

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

而在 fn1 函數(shù)代碼的執(zhí)行過程中(執(zhí)行階段),變量對象變?yōu)榛顒訉ο?,原先聲明的變量會被賦值,其活動對象如下所示

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

對于全局上下文來說,由于其不會有參數(shù)傳遞,所以在生成變量對象的過程中只有檢索當(dāng)前上下文中的函數(shù)聲明和檢索當(dāng)前上下文中的變量聲明兩個步驟。

在瀏覽器環(huán)境中,全局上下文中的變量對象(全局對象)即我們熟悉的 window 對象,通過該對象可以使用其預(yù)定義的變量和函數(shù),在全局環(huán)境中所聲明的變量和函數(shù),也會成為全局對象的屬性。

弄明白了變量對象的生成過程后,我們就能夠更深入地理解函數(shù)提升以及變量提升的內(nèi)在機制了。

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

上述代碼中,在全局上下文的創(chuàng)建階段,會檢索上下文中的函數(shù)聲明以及變量聲明,函數(shù)會被賦值具體的引用地址而變量會被賦值為 undefined。

所以上述代碼實際上的運行過程如下

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

所以,這就是我們經(jīng)常提到的函數(shù)提升以及變量提升的內(nèi)在機制。

作用域鏈(Scope Chain)

作用域鏈?zhǔn)侵赣僧?dāng)前上下文和上層上下文的一系列變量對象組成的層級鏈。它保證了當(dāng)前執(zhí)行環(huán)境對符合訪問權(quán)限的變量和函數(shù)的有序訪問。

我們已經(jīng)知道,執(zhí)行上下文分為創(chuàng)建和執(zhí)行兩個階段,在執(zhí)行上下文的執(zhí)行階段,當(dāng)需要查找某個變量或函數(shù)時,會在當(dāng)前上下文的變量對象(活動對象)中進行查找,若是沒有找到,則會沿著上層上下文的變量對象進行查找,直到全局上下文中的變量對象(全局對象)。

那么當(dāng)前上下文是如何有序地去查找它所需要的變量或函數(shù)的呢?

答案就是依靠當(dāng)前上下文中的作用域鏈,其包含了當(dāng)前上下文和上層上下文中的變量對象,以便其一層一層地去查找其所需要的變量和函數(shù)。

執(zhí)行上下文中的作用域鏈又是怎么建立的呢?

我們都知道,JavaScript 中主要包含了全局作用域和函數(shù)作用域,而函數(shù)作用域是在函數(shù)被聲明的時候確定的。

每一個函數(shù)都會包含一個 [[scope]] 內(nèi)部屬性,在函數(shù)被聲明的時候,該函數(shù)的 [[scope]] 屬性會保存其上層上下文的變量對象,形成包含上層上下文變量對象的層級鏈。[[scope]] 屬性的值是在函數(shù)被聲明的時候確定的。

當(dāng)函數(shù)被調(diào)用的時候,其執(zhí)行上下文會被創(chuàng)建并入棧。在創(chuàng)建階段生成其變量對象后,會將該變量對象添加到作用域鏈的頂端并將 [[scope]] 添加進該作用域鏈中。而在執(zhí)行階段,變量對象會變?yōu)榛顒訉ο?,其相?yīng)屬性會被賦值。

所以,作用域鏈?zhǔn)怯僧?dāng)前上下文變量對象及上層上下文變量對象組成的

SC = AO + [[scope]]

讓我們來看個栗子~

var a = 1;
function fn1() {
  var b = 1;
  function fn2() {
    var c = 1;
  }
  fn2();
}
fn1();

在 fn1 函數(shù)上下文中,fn2 函數(shù)被聲明,所以

fn2.[[scope]]=[fn1_EC.VO, globalObj]

當(dāng) fn2 被調(diào)用的時候,其執(zhí)行上下文被創(chuàng)建并入棧,此時會將生成的變量對象添加進作用域鏈的頂端,并且將 [[scope]] 添加進作用域鏈

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

this 指向

this 的指向,是在函數(shù)被調(diào)用的時候確定的。也就是執(zhí)行上下文被創(chuàng)建時確定的。

關(guān)于 this 的指向,其實最主要的是三種場景,分別是全局上下文中 this、函數(shù)中 this構(gòu)造函數(shù)中 this

全局上下文中 this

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

// 在瀏覽器環(huán)境中,全局對象是 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
函數(shù)中 this

函數(shù)中的 this 指向是怎樣一種情況呢?

如果被調(diào)用的函數(shù),被某一個對象所擁有,那么其內(nèi)部的 this 指向該對象;如果該函數(shù)被獨立調(diào)用,那么其內(nèi)部的 this 指向 undefined(非嚴(yán)格模式下指向 window)。

舉個栗子~

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

上述代碼中 fn 函數(shù)都是輸出 this.a,根據(jù)上述的結(jié)論,obj.fn() 由于其是被 obj 對象所擁有,所以 this 指向 obj 對象;而 fn 是被獨立調(diào)用,在非嚴(yán)格模式下 this 指向 window。

構(gòu)造函數(shù)中 this

要清楚構(gòu)造函數(shù)中 this 的指向,則必須先了解通過 new 操作符調(diào)用構(gòu)造函數(shù)時所經(jīng)歷的階段。

通過 new 操作符調(diào)用構(gòu)造函數(shù)時所經(jīng)歷的階段如下:

  1. 創(chuàng)建一個新對象;
  2. 將構(gòu)造函數(shù)的 this 指向這個新對象;
  3. 執(zhí)行構(gòu)造函數(shù)內(nèi)部代碼;
  4. 返回這個新對象。

所以從上述流程可知,對于構(gòu)造函數(shù)來說,其內(nèi)部 this 指向新創(chuàng)建的對象實例。

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 中箭頭函數(shù)中,this 是在函數(shù)聲明的時候確定的,具體可看 http://es6.ruanyifeng.com/#docs/function

一個完整的栗子

接下來,讓我們來完整地 look 一下程序運行過程中執(zhí)行上下文及其內(nèi)部屬性的變化情況。

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

上述代碼在執(zhí)行過程中,執(zhí)行上下文棧的變化過程如下

/* 偽代碼 以數(shù)組來表示執(zhí)行上下文棧 ECStack=[] */
// 代碼執(zhí)行時最先進入全局環(huán)境,全局上下文被創(chuàng)建并入棧
ECStack.push(global_EC);
// fn1 被調(diào)用,fn1 函數(shù)上下文被創(chuàng)建并入棧
ECStack.push(fn1_EC);
// fn1 中調(diào)用 fn2,fn2 函數(shù)上下文被創(chuàng)建并入棧
ECStack.push(fn2_EC);
// fn2 執(zhí)行完畢,fn2 函數(shù)上下文出棧
ECStack.pop();
// fn1 執(zhí)行完畢,fn1 函數(shù)上下文出棧
ECStack.pop();
// 代碼執(zhí)行完畢,全局上下文出棧
ECStack.pop();
首先進入全局環(huán)境,全局上下文被創(chuàng)建并入棧

全局上下文如下

global_EC = {
  VO: globalObj,
  SC: [globalObj],
  this: globalObj,
}
接著 fn1 被調(diào)用,fn1 函數(shù)上下文被創(chuàng)建并入棧

在 fn1 函數(shù)上下文被創(chuàng)建之前,會有一個函數(shù)聲明過程,這個過程發(fā)生在全局上下文創(chuàng)建階段,在這個過程中,fn1.[[scope]] 會保存其上層作用域的變量對象。

在 fn1 函數(shù)上下文創(chuàng)建階段,其執(zhí)行上下文如下

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

在 fn1 函數(shù)上下文執(zhí)行階段,其執(zhí)行上下文如下

fn1_EC = {
  VO: {
    Arguments: {
      length: 0
    },
    fn2: <function fn2 reference>,
    a:1
  },
  SC:[fn1_EC.VO, globalObj],
  this:globalObj
}
然后在 fn1 中調(diào)用 fn2,fn2 函數(shù)上下文被創(chuàng)建并入棧

在 fn2 函數(shù)上下文創(chuàng)建階段,其執(zhí)行上下文如下

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

在 fn2 函數(shù)上下文執(zhí)行階段,其執(zhí)行上下文如下

fn2_EC = {
  VO: {
    Arguments: {
      '0': 2,
      length: 0
    },
    b: 2,
    c: 3
  },
  SC: [fn2_EC.VO, fn1_EC.VO, globalObj],
  this: globalObj
}
最后是各個上下文出棧

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

而我們經(jīng)常說閉包能夠訪問其所在環(huán)境的變量,其實是因為閉包能夠阻止上述變量對象被回收的過程。

深入地理解了執(zhí)行上下文的內(nèi)容后,對于我們理解閉包也會有很大的幫助,關(guān)于閉包我寫過一篇 《 JavaScript 閉包詳解 》,感興趣的童鞋也可以繼續(xù)閱讀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容