首先,回顧下上篇博文中 "js基礎梳理 究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?" 的執行上下文的生命周期: 3.執行上下文的生命周期 3.1 創建階段 生成變數對象(Variable object, VO) 建立作用域鏈(Scope chain) 確定this指向 3.2 執行 ...
首先,回顧下上篇博文中js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?的執行上下文的生命周期:
3.執行上下文的生命周期
3.1 創建階段
- 生成變數對象(Variable object, VO)
- 建立作用域鏈(Scope chain)
- 確定this指向
3.2 執行階段
- 變數賦值
- 函數引用
- 執行其他代碼
1.什麼是變數對象(Variable Object)
在寫程式的時候會定義很多變數和函數,那js解析器是如何找到這些變數和函數的?
變數對象是與執行上下文對應的概念,在執行上下文的創建階段,它依次存儲著在上下文中定義的以下內容:
1.1 函數的所有形參(如果是函數上下文中):
建立arguments對象。檢查當前上下文中的參數,建立該對象下的屬性與屬性值。沒有實參的話,屬性值為undefined。
1.2. 所有函數聲明:(FunctionDeclaration, FD)
檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變數對象中以函數名建立一個屬性,屬性值為指向該函數所在記憶體地址的引用。如果變數對象已經存在相同名稱的屬性,則完全替換這個屬性。
1.3. 所有變數聲明:(var, VariableDeclaration)
檢查當前上下文中的變數聲明,每找到一個變數聲明,就在變數對象中以變數名建立一個屬性,屬性值為undefined。如果變數名稱跟已經聲明的形式參數或函數相同,則變數聲明不會幹擾已經存在的這類屬性。
2.什麼是活動對象?(activation object, AO)
只有全局上下文的變數對象允許通過VO的屬性名稱來間接訪問,在其他上下文(後面乾脆直接講函數上下文吧,我們並沒有分析eval上下文)中是不能直接訪問VO對象的。
在函數上下文中,VO是不能直接訪問的,此時由活動對象AO繼續扮演VO的角色。
未進入執行階段前,變數對象中的屬性都不能訪問!但是進入到執行階段之後,變數對象轉變成了活動對象,裡面的屬性都能被訪問了,然後開始進行執行階段的操作。
因此,對於函數上下文來講,活動對象與變數對象其實都是同一個對象,只是處於執行上下文的不同生命周期。不過只有處於執行上下文棧棧頂的函數執行上下文中的變數對象,才會變成活動對象。
3.舉個例子
說了一堆概念,有點懵,對嗎?請看這個例子:
var a = 10;
function b () {
console.log('全局的b函數')
};
function bar(a, b) {
console.log('1', a, b)
var a = 1
function b() {
console.log('bar下的b函數')
}
console.log('2', a, b)
}
bar(2, 3)
console.log('3', a, b)
要想知道為什麼會這樣列印,首先,從執行上下文的創建階段來分析變數對象:
// 創建階段:
// 第一步,遇到了全局代碼,進入全局上下文,此時的執行上下文棧是這樣
ECStack = [
globalContext: {
VO: {
// 根據1.2,會優先處理全局下的b函數聲明,值為該函數所在記憶體地址的引用
b: <reference to function>,
// 緊接著,按順序再處理bar函數聲明,此時根據1.1,因為是在全局上下文中,並不會分析bar函數的參數
bar: <refernce to function>,
// 根據1.3,再處理變數,並賦值為undefined
a: undefined
}
}
];
// 第二步,發現bar函數被調用,就又創建了一個函數上下文,此時的執行上下文棧是這樣
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: undefined
}
},
<bar>functionContext: {
VO: {
// 根據1.1,優先分析函數的形參
arguments: {
0: 2,
1: 3,
length: 2,
callee: bar
},
a: 2,
// b: 3,
// 根據1.2, 再分析bar函數中的函數聲明b,並且賦值為b函數所在記憶體地址的引用, 它發現VO中已經有b:3了,就會覆蓋掉它。因此上面一行中的b:3實際上不存在了。
b: <refernce to function b() {}>
// 根據1.3,接著分析bar函數中的變數聲明a,並且賦值為undefined, 但是發現VO中已經有a:2了,因此下麵一行中的a:undefined也是會不存在的。
// a: undefined
}
}
]
以上就是執行上下文中的代碼分析階段,也就是執行上下文的創建階段。再看看執行上下文的代碼執行階又發生了什麼。
// 執行階段:
// 第三步:首先,執行了bar(2, 3)函數,緊接著,在bar函數里執行了console.log('1', a, b)。全局上下文中依然還是VO,但是函數上下文中VO就變成了AO。並且代碼執行到這,就已經修改了全局上下文中的變數a.
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: 10,
}
},
<bar>functionContext: {
AO: {
arguments: {
0: 2,
1: 3,
length: 2,
callee: bar
},
a: 2,
b: <refernce to function b() {}>
}
}
]
// 因此會輸出結果: '1', 2, function b() {console.log('bar下的b函數')};
// 第四步:執行console.log('2', a, b)的時候, 發現裡面的變數a被重新賦值為1了。
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: 10,
}
},
<bar>functionContext: {
AO: {
arguments: {
0: 2,
1: 3,
length: 2,
callee: bar
},
a: 1,
b: <refernce to function b() {}>
}
}
]
// 因此會輸出結果: '2', 1, function b() {console.log('bar下的b函數')};
// 第五步,執行到console.log('3', a, b)的時候,ECStack發現bar函數已經執行完了,就把bar從ECStack給彈出去了。此時的執行上下文棧是這樣的。
ECStack = [
globalContext: {
VO: {
b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: 10,
}
}
]
// 因此會輸出結果: '3', 10, function b() {console.log('全局的b函數')}
總結一下,變數對象會有以下四種特性:
- 全局上下文的變數對象初始化是全局對象(其實這篇文章並沒有介紹這個特性,不過它也很簡單就這麼一句話而已)
- 函數上下文的變數對象初始化只包括Arguments對象
- 在進入執行上下文的時候會給變數對象添加形參,函數聲明,變數聲明等初始的屬性值
- 在代碼執行階段,會再次修改變數對象的屬性值。
理解了這些,是不是發現再有一些函數提升,變數提升什麼的是不是都很簡單了。例如,你可以思考下這三段代碼分別發生了什麼。
foo()
var foo = function() {console.log(1)}
function foo() {console.log(2)}
foo()
function foo() {console.log(2)}
var foo = function() {console.log(1)}
var foo = function() {console.log(1)}
function foo() {console.log(2)}
foo()