首頁»JavaScript»解密JavaScript執行上下文

解密JavaScript執行上下文

來源:玩弄心里的鬼 發布時間:2019-05-22 閱讀次數:

 執行上下文棧

  首先我們先了解一下什么是執行上下文棧(Execution context stack)。

  上面這張圖來自于mdn,分別展示了棧、堆和隊列,其中棧就是我們所說的執行上下文棧;堆是用于存儲對象這種復雜類型,我們復制對象的地址引用就是這個堆內存的地址;隊列就是異步隊列,用于event loop的執行。

  JS代碼在引擎中是以“一段一段”的方式來分析執行的,而并非一行一行來分析執行。而這“一段一段”的可執行代碼無非為三種:Global code、Function Code、Eval code。這些可執行代碼在執行的時候又會創建一個一個的執行上下文(Execution context)。例如,當執行到一個函數的時候,JS引擎會做一些“準備工作”,而這個“準備工作”,我們稱其為執行上下文。

  那么隨著我們的執行上下文數量的增加,JS引擎又如何去管理這些執行上下文呢?這時便有了執行上下文棧。

  這里我用一段貫穿全文的例子來講解執行上下文棧的執行過程:

var scope = 'global scope';
function checkscope(s) {
  var scope = 'local scope';
  function f() {
    return scope;
  }
  return f();
}
checkscope('scope');

  當JS引擎去解析代碼的時候,最先碰到的就是Global code,所以一開始初始化的時候便會將全局上下文推入執行上下文棧,并且只有在整個應用程序執行完畢的時候,全局上下文才會推出執行上下文棧。

  這里我們用ECS來模擬執行上下文棧,用globalContext來表示全局上下文:

ESC = [
   globalContext, // 一開始只有全局上下文
]

  然后當代碼執行checkscope函數的時候,會創建checkscope函數的執行上下文,并將其壓入執行上下文棧:

ESC = [
  checkscopeContext, // checkscopeContext入棧
  globalContext,
]

  接著代碼執行到return f()的時候,f函數的執行上下文被創建:

ESC = [
  fContext, // fContext入棧
  checkscopeContext,
  globalContext,
]

  f函數執行完畢后,f函數的執行上下文出棧,隨后checkscope函數執行完畢,checkscope函數的執行上下文出棧:

// fContext出棧
ESC = [
  // fContext出棧
  checkscopeContext,
  globalContext,
]
// checkscopeContext出棧
ESC = [
  // checkscopeContext出棧
  globalContext,
]

 變量對象

  每一個執行上下文都有三個重要的屬性:

  • 變量對象
  • 作用域鏈
  • this

  這一節我們先來說一下變量對象(Variable object,這里簡稱VO)。

  變量對象是與執行上下文相關的數據作用域,存儲了在上下文中定義的變量和函數聲明。并且不同的執行上下文也有著不同的變量對象,這里分為全局上下文中的變量對象和函數執行上下文中的變量對象。

  全局上下文中的變量對象

  全局上下文中的變量對象其實就是全局對象。我們可以通過this來訪問全局對象,并且在瀏覽器環境中,this === window;在node環境中,this === global。

  函數上下文中的變量對象

  在函數上下文中的變量對象,我們用活動對象來表示(activation object,這里簡稱AO),為什么稱其為活動對象呢,因為只有到當進入一個執行上下文中,這個執行上下文的變量對象才會被激活,并且只有被激活的變量對象,其屬性才能被訪問。

  在函數執行之前,會為當前函數創建執行上下文,并且在此時,會創建變量對象:

  • 根據函數arguments屬性初始化arguments對象;
  • 根據函數聲明生成對應的屬性,其值為一個指向內存中函數的引用指針。如果函數名稱已存在,則覆蓋;
  • 根據變量聲明生成對應的屬性,此時初始值為undefined。如果變量名已聲明,則忽略該變量聲明;

  還是以剛才的代碼為例:

var scope = 'global scope';
function checkscope(s) {
  var scope = 'local scope';
  function f() {
    return scope;
  }
  return f();
}
checkscope('scope');

  在執行checkscope函數之前,會為其創建執行上下文,并初始化變量對象,此時的變量對象為:

VO = {
  arguments: {
    0: 'scope',
    length: 1,
  },
  s: 'scope', // 傳入的參數
  f: pointer to function f(),
  scope: undefined, // 此時聲明的變量為undefined
}

  隨著checkscope函數的執行,變量對象被激活,變相對象內的屬性隨著代碼的執行而改變:

VO = {
  arguments: {
    0: 'scope',
    length: 1,
  },
  s: 'scope', // 傳入的參數
  f: pointer to function f(),
  scope: 'local scope', // 變量賦值
}

  其實也可以用另一個概念“函數提升”和“變量提升”來解釋:

function checkscope(s) {
  function f() { // 函數提升
    return scope;
  }
  var scope; // 變量聲明提升	
  scope = 'local scope' // 變量對象的激活也相當于此時的變量賦值
  return f();
}

 作用域鏈

  每一個執行上下文都有三個重要的屬性:

  • 變量對象
  • 作用域鏈
  • this

  這一節我們說一下作用域鏈。

  什么是作用域鏈

  當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級執行上下文的變量對象中查找,一直找到全局上下文的變量對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈。

  下面還是用我們的例子來講解作用域鏈:

var scope = 'global scope';	
function checkscope(s) {
  var scope = 'local scope';	
  function f() {
    return scope;
  }
  return f();
}
checkscope('scope');

  首先在checkscope函數聲明的時候,內部會綁定一個[[scope]]的內部屬性:

checkscope.[[scope]] = [
  globalContext.VO
];

  接著在checkscope函數執行之前,創建執行上下文checkscopeContext,并推入執行上下文棧:

  • 復制函數的[[scope]]屬性初始化作用域鏈;
  • 創建變量對象;
  • 將變量對象壓入作用域鏈的最頂端;
     
// -> 初始化作用域鏈;
checkscopeContext = {
  scope: checkscope.[[scope]],
}

// -> 創建變量對象
checkscopeContext = {
  scope: checkscope.[[scope]],
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 傳入的參數
    f: pointer to function f(),
    scope: undefined, // 此時聲明的變量為undefined
  },
}

// -> 將變量對象壓入作用域鏈的最頂端
checkscopeContext = {
  scope: [VO, checkscope.[[scope]]],
  VO = {
    arguments: {
    0: 'scope',
    length: 1,
  },
  s: 'scope', // 傳入的參數
  f: pointer to function f(),
  scope: undefined, // 此時聲明的變量為undefined
  },
}

  接著,隨著函數的執行,修改變量對象:

checkscopeContext = {
  scope: [VO, checkscope.[[scope]]],
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 傳入的參數 
    f: pointer to function f(),
    scope: 'local scope', // 變量賦值
  }
}

  與此同時遇到f函數聲明,f函數綁定[[scope]]屬性:

checkscope.[[scope]] = [
  checkscopeContext.VO, // f函數的作用域還包括checkscope的變量對象
  globalContext.VO
];

  之后f函數的步驟同checkscope函數。

  再來一個經典的例子:

var data = [];	
for (var i = 0; i < 6; i++) {
  data[i] = function () {
    console.log(i); 
  }; 
}
data[0]();
// ...

  很簡單,不管訪問data幾,最終console打印出來的都是6,因為在ES6之前,JS都沒有塊級作用域的概念,for循環內的代碼都在全局作用域下。

  在data函數執行之前,此時全局上下文的變量對象為:

globalContext.VO = { 
  data: [pointer to function ()], 
  i: 6, // 注意:此時的i值為6 
}

  每一個data匿名函數的執行上下文鏈大致都如下:

data[n]Context = {
  scope: [VO, globalContext.VO],
  VO: {
    arguments: {
      length: 0,
    }
  }
}

  那么在函數執行的時候,會先去自己匿名函數的變量對象上找i的值,發現沒有后會沿著作用域鏈查找,找到了全局執行上下文的變量對象,而此時全局執行上下文的變量對象中的i為6,所以每一次都打印的是6了。

  詞法作用域 & 動態作用域

  JavaScript這門語言是基于詞法作用域來創建作用域的,也就是說一個函數的作用域在函數聲明的時候就已經確定了,而不是函數執行的時候。

  改一下之前的例子:

var scope = 'global scope';
function f() {
  console.log(scope)
}
function checkscope() {
  var scope = 'local scope';
  f();
}
checkscope();

  因為JavaScript是基于詞法作用域創建作用域的,所以打印的結果是global scope而不是local scope。我們結合上面的作用域鏈來分析一下:

  首先遇到了f函數的聲明,此時為其綁定[[scope]]屬性:

// 這里就是我們所說的“一個函數的作用域在函數聲明的時候就已經確定了”
f.[[scope]] = [
  globalContext.VO, // 此時的全局上下文的變量對象中保存著scope = 'global scope';
];

  然后我們直接跳過checkscope的執行上下文的創建和執行的過程,直接來到f函數的執行上。此時在函數執行之前初始化f函數的執行上下文:

// 這里就是為什么會打印global scope
fContext = {
  scope: [VO, globalContext.VO], // 復制f.[[scope]],f.[[scope]]只有全局執行上下文的變量對象
  VO = {
    arguments: {
      length: 0,
    },
  },
}

  然后到了f函數執行的過程,console.log(scope),會沿著f函數的作用域鏈查找scope變量,先是去自己執行上下文的變量對象中查找,沒有找到,然后去global執行上下文的變量對象上查找,此時scope的值為global scope。

 this

  在這里this綁定也可以分為全局執行上下文和函數執行上下文:

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

  總結起來就是,誰調用了,this就指向誰。

 執行上下文

  這里,根據之前的例子來完整的走一遍執行上下文的流程:

var scope = 'global scope';
function checkscope(s) {
  var scope = 'local scope';
  function f() {
    return scope;
  }
  return f();
}
checkscope('scope');

  首先,執行全局代碼,創建全局執行上下文,并且全局執行上下文進入執行上下文棧:

globalContext = {
  scope: [globalContext.VO],
  VO: global,
  this: globalContext.VO
}
ESC = [
  globalContext,
]

  然后隨著代碼的執行,走到了checkscope函數聲明的階段,此時綁定[[scope]]屬性:

checkscope.[[scope]] = [
  globalContext.VO,
]

  在checkscope函數執行之前,創建checkscope函數的執行上下文,并且checkscope執行上下文入棧:

// 創建執行上下文
checkscopeContext = {
  scope: [VO, globalContext.VO], // 復制[[scope]]屬性,然后VO推入作用域鏈頂端
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 傳入的參數
    f: pointer to function f(),
    scope: undefined,
  },
  this: globalContext.VO,
}

// 進入執行上下文棧
ESC = [
  checkscopeContext,
  globalContext,
]

  checkscope函數執行,更新變量對象:

// 創建執行上下文
checkscopeContext = {
  scope: [VO, globalContext.VO], // 復制[[scope]]屬性,然后VO推入作用域鏈頂端
  VO = {
    arguments: {
      0: 'scope',
      length: 1,
    },
    s: 'scope', // 傳入的參數
    f: pointer to function f(),
    scope: 'local scope', // 更新變量
  },
  this: globalContext.VO,
}

  f函數聲明,綁定[[scope]]屬性:

f.[[scope]] = [
  checkscopeContext.VO,
  globalContext.VO,
]

  f函數執行,創建執行上下文,推入執行上下文棧:

// 創建執行上下文
fContext = {
  scope: [VO, checkscopeContext.VO, globalContext.VO], // 復制[[scope]]屬性,然后VO推入作用域鏈頂端
  VO = {
    arguments: {
      length: 0,
    },
  },
  this: globalContext.VO,
}
	
// 入棧
ESC = [
  fContext,
  checkscopeContext,
  globalContext,
]

  f函數執行完成,f函數執行上下文出棧,checkscope函數執行完成,checkscope函數出棧:

ESC = [
  // fContext出棧
  checkscopeContext,
  globalContext,
]

ESC = [
  // checkscopeContext出棧,
  globalContext,
]

  到此,一個整體的執行上下文的流程就分析完了。

QQ群:WEB開發者官方群(515171538),驗證消息:10000
微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
網友評論(共0條評論) 正在載入評論......
理智評論文明上網,拒絕惡意謾罵 發表評論 / 共0條評論
登錄會員中心
大神带着买彩票 手游| 临武县| 海晏县| 朝阳市| 股票| 孝义市| 瓦房店市| 阳泉市| 逊克县| 上栗县| 越西县| 沙坪坝区| 定安县| 香港| 张北县| 库尔勒市| 隆安县| 河曲县| 大渡口区| 攀枝花市| 同心县| 阳山县| 涿鹿县| 舟曲县| 惠来县| 弥渡县| 广丰县| 石河子市| 重庆市| 邵东县| 林州市| 同心县| 雅安市| 勃利县| 四平市| 大田县| 沂水县| 宁远县|