解密 JavaScript 執行上下文

執行上下文棧

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

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

JS代碼在引擎中是以「一段一段」的方式來分析執行的,而並不是一行一行來分析執行。而這「一段一段」的可執行代碼無非爲三種:Global codeFunction CodeEval code。這些可執行代碼在執行的時候又會建立一個一個的執行上下文(Execution context)。例如,當執行到一個函數的時候,JS引擎會作一些「準備工做」,而這個「準備工做」,咱們稱其爲執行上下文java

那麼隨着咱們的執行上下文數量的增長,JS引擎又如何去管理這些執行上下文呢?這時便有了執行上下文棧。node

這裏我用一段貫穿全文的例子來說解執行上下文棧的執行過程:瀏覽器

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函數的執行上下文,並將其壓入執行上下文棧:oop

ESC = [
  checkscopeContext, // checkscopeContext入棧
  globalContext,
]
複製代碼

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

ESC = [
  fContext, // fContext入棧
  checkscopeContext,
  globalContext,
]
複製代碼

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

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

// checkscopeContext出棧
ESC = [
  // checkscopeContext出棧
  globalContext,
]
複製代碼

變量對象

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

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

這一節咱們先來講一下變量對象(Variable object,這裏簡稱VO)。

變量對象是與執行上下文相關的數據做用域,存儲了在上下文中定義的變量和函數聲明。而且不一樣的執行上下文也有着不一樣的變量對象,這裏分爲全局上下文中的變量對象和函數執行上下文中的變量對象。

全局上下文中的變量對象

全局上下文中的變量對象其實就是全局對象。咱們能夠經過this來訪問全局對象,而且在瀏覽器環境中,this === window;在node環境中,this === global

this === window

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,
]
複製代碼

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

相關文章
相關標籤/搜索