首先咱們先了解一下什麼是執行上下文棧(Execution context stack)。javascript
上面這張圖來自於mdn,分別展現了棧、堆和隊列,其中棧就是咱們所說的執行上下文棧;堆是用於存儲對象這種複雜類型,咱們複製對象的地址引用就是這個堆內存的地址;隊列就是異步隊列,用於event loop的執行。java
JS代碼在引擎中是以「一段一段」的方式來分析執行的,而並不是一行一行來分析執行。而這「一段一段」的可執行代碼無非爲三種:Global code
、Function Code
、Eval code
。這些可執行代碼在執行的時候又會建立一個一個的執行上下文(Execution context)。例如,當執行到一個函數的時候,JS引擎會作一些「準備工做」,而這個「準備工做」,咱們稱其爲執行上下文
。node
那麼隨着咱們的執行上下文數量的增長,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來表示全局上下文:oop
ESC = [ globalContext, // 一開始只有全局上下文 ]
而後當代碼執行checkscope函數的時候,會建立checkscope函數的執行上下文,並將其壓入執行上下文棧:this
ESC = [ checkscopeContext, // checkscopeContext入棧 globalContext, ]
接着代碼執行到return f()
的時候,f函數的執行上下文被建立:spa
ESC = [ fContext, // fContext入棧 checkscopeContext, globalContext, ]
f函數執行完畢後,f函數的執行上下文出棧,隨後checkscope函數執行完畢,checkscope函數的執行上下文出棧:指針
// fContext出棧 ESC = [ // fContext出棧 checkscopeContext, globalContext, ] // checkscopeContext出棧 ESC = [ // checkscopeContext出棧 globalContext, ]
每個執行上下文都有三個重要的屬性:
這一節咱們先來講一下變量對象(Variable object,這裏簡稱VO)。
變量對象是與執行上下文相關的數據做用域,存儲了在上下文中定義的變量和函數聲明。而且不一樣的執行上下文也有着不一樣的變量對象,這裏分爲全局上下文中的變量對象和函數執行上下文中的變量對象。
全局上下文中的變量對象其實就是全局對象。咱們能夠經過this來訪問全局對象,而且在瀏覽器環境中,this === window
;在node環境中,this === global
。
在函數上下文中的變量對象,咱們用活動對象來表示(activation object,這裏簡稱AO),爲何稱其爲活動對象呢,由於只有到當進入一個執行上下文中,這個執行上下文的變量對象纔會被激活,而且只有被激活的變量對象,其屬性才能被訪問。
在函數執行以前,會爲當前函數建立執行上下文,而且在此時,會建立變量對象:
仍是以剛纔的代碼爲例:
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(); }
每個執行上下文都有三個重要的屬性:
這一節咱們說一下做用域鏈。
當查找變量的時候,會先從當前上下文的變量對象中查找,若是沒有找到,就會從父級執行上下文的變量對象中查找,一直找到全局上下文的變量對象。這樣由多個執行上下文的變量對象構成的鏈表就叫作做用域鏈。
下面仍是用咱們的例子來說解做用域鏈:
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就指向誰。
這裏,根據以前的例子來完整的走一遍執行上下文的流程:
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, ]
到此,一個總體的執行上下文的流程就分析完了。