首先咱們先了解一下什麼是執行上下文棧(Execution context stack)。javascript
上面這張圖來自於mdn,分別展現了棧、堆和隊列,其中棧就是咱們所說的執行上下文棧;堆是用於存儲對象這種複雜類型,咱們複製對象的地址引用就是這個堆內存的地址;隊列就是異步隊列,用於event loop的執行。JS代碼在引擎中是以「一段一段」的方式來分析執行的,而並不是一行一行來分析執行。而這「一段一段」的可執行代碼無非爲三種:Global code
、Function Code
、Eval 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
這一節咱們先來講一下變量對象(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,
]
複製代碼
到此,一個總體的執行上下文的流程就分析完了。