在js中,執(zhí)行上下文(Execution Context)是非常重要的一種對(duì)象,它保存著函數(shù)執(zhí)行所需的重要信息,其中有三個(gè)屬性:變量對(duì)象(variable object),作用域鏈(scope chain),this指針(this value),它們影響著變量的解析,變量作用域和函數(shù)this的指向。
上下文棧(Execution Context Stack)
js執(zhí)行的時(shí)候會(huì)維護(hù)一個(gè)上下文棧,每次開始執(zhí)行一個(gè)函數(shù)之前,js都要?jiǎng)?chuàng)建一個(gè)上下文對(duì)象,并將其壓入上下文棧中。因此,當(dāng)前正在執(zhí)行的函數(shù)的上下文(簡(jiǎn)稱當(dāng)前上下文)總是在棧頂,這個(gè)函數(shù)一執(zhí)行完,上下文就會(huì)從棧中彈出。
代碼開始運(yùn)行時(shí),棧頂會(huì)先放入一個(gè)全局上下文,全局上下文同樣有變量對(duì)象,作用域鏈和this指針。
舉個(gè)例子:
function fun2() {
var b = 222;
}
function fun1() {
var a = 111;
fun2();
}
fun1();
c = 333;
- 開始執(zhí)行時(shí):
棧頂:全局上下文 - 代碼執(zhí)行到
fun1();
時(shí),創(chuàng)建fun1的上下文,壓入棧。這時(shí)棧變成:
棧頂:fun1上下文 / 全局上下文 - 代碼執(zhí)行到
fun2();
時(shí),創(chuàng)建fun2的上下文,壓入棧。這時(shí)棧變成:
棧頂:fun2上下文 / fun1上下文 / 全局上下文 - fun2執(zhí)行完畢,fun2上下文彈出:
棧頂:fun1上下文 / 全局上下文
(這時(shí)執(zhí)行權(quán)回到fun1內(nèi),棧頂也恰好回到了fun1上下文。可見這種機(jī)制能保證正在執(zhí)行的函數(shù)的上下文總是在棧頂) - fun1執(zhí)行完畢,fun1上下文彈出,執(zhí)行權(quán)回到全局區(qū)域:
棧頂:全局上下文 - 所有代碼都執(zhí)行完畢,全局上下文彈出,棧為空。
執(zhí)行上下文.變量對(duì)象(Variable Object)
我們已經(jīng)說過,每次執(zhí)行(注意是執(zhí)行而不是聲明!)一個(gè)函數(shù)之前,執(zhí)行引擎都會(huì)創(chuàng)建一個(gè)上下文對(duì)象。創(chuàng)建上下文對(duì)象的時(shí)候,就會(huì)創(chuàng)建它的一個(gè)重要屬性:變量對(duì)象。
創(chuàng)建變量對(duì)象的過程是這樣:
- 建立arguments對(duì)象:屬性名是'0'、'1'、'2'.....,屬性值就是實(shí)際傳入的參數(shù)。此外arguments.length是實(shí)際參數(shù)的個(gè)數(shù)。
- 找到這個(gè)將要執(zhí)行的函數(shù)內(nèi)的所有函數(shù)聲明,儲(chǔ)存在變量對(duì)象中,屬性名就是函數(shù)名,屬性值就是函數(shù)的引用(所在的內(nèi)存地址)。如果有多個(gè)同名的函數(shù)聲明,后出現(xiàn)的函數(shù)覆蓋前面的屬性值。
- 找到這個(gè)將要執(zhí)行的函數(shù)內(nèi)的所有變量聲明,儲(chǔ)存在變量對(duì)象中,屬性名就是變量名,屬性值是undefined。
還有一個(gè)概念叫做活動(dòng)對(duì)象(activation object),活動(dòng)對(duì)象其實(shí)和變量對(duì)象是同一個(gè)東西在不同時(shí)期的兩種叫法。函數(shù)還未開始執(zhí)行(創(chuàng)建上下文的期間)時(shí)叫變量對(duì)象,函數(shù)開始執(zhí)行以后就叫活動(dòng)對(duì)象。
變量對(duì)象讓js有變量提升的特性
原來,在js執(zhí)行函數(shù)之前,會(huì)先掃描一遍代碼,將變量、函數(shù)聲明都放到變量對(duì)象中。當(dāng)執(zhí)行函數(shù)時(shí)如果遇到一個(gè)變量、函數(shù)名,就會(huì)到活動(dòng)對(duì)象中去找,發(fā)現(xiàn)有對(duì)應(yīng)的屬性名,就可以從變量對(duì)象中取出它的屬性值來使用,而不用等到那一句聲明語句之后!
例子:
function fun1(var arg) {
// 創(chuàng)建變量對(duì)象:{arg:987, fun2:fun2的地址, a:undefinded}
console.log(a); // 打印undefinded,因?yàn)榛顒?dòng)對(duì)象中有鍵值對(duì):a:undefinded。
var a = 111; // 如果將這一語句刪除,上一句會(huì)直接報(bào)錯(cuò)!
console.log(a); // 打印111,因?yàn)榛顒?dòng)對(duì)象中有鍵值對(duì):a:111
fun2(); // 打印in fun2! 因?yàn)榛顒?dòng)對(duì)象中有鍵值對(duì):fun2:某個(gè)內(nèi)存地址
return; // 即使是在return之后的聲明,也會(huì)被放入變量對(duì)象!
function fun2() {
console.log('in fun2!');
}
}
fun1(987);
// 輸出為:
// undefined
// 111
// in fun2!
為什么js不是塊級(jí)作用域
這通過變量對(duì)象就可以解釋了。因?yàn)橹灰谕粋€(gè)函數(shù)中聲明,所有變量都會(huì)保存在同一個(gè)變量對(duì)象中!所以在代碼塊中聲明變量和在代碼塊外聲明變量當(dāng)然沒有區(qū)別!
還記得我們剛才說“代碼開始運(yùn)行時(shí),棧頂會(huì)先放入一個(gè)全局上下文”嗎?在瀏覽器中,全局上下文的變量對(duì)象就是全局對(duì)象(就是window對(duì)象)!這就可以解釋以下代碼:
// 執(zhí)行函數(shù)之前發(fā)現(xiàn)a的聲明,將其加入window對(duì)象,設(shè)為undefined
var a = 111; // 在活動(dòng)對(duì)象(也就是window)中將a賦值為111
console.log(window.a); // 打印111
window.a = 111;
console.log(a); // 打印111,因?yàn)樵诨顒?dòng)對(duì)象(也就是window)中找到了a:111
經(jīng)過測(cè)試,Node.js全局上下文的變量對(duì)象不是全局對(duì)象(global)。
var a = 111;
console.log(global.a); // 打印undefined
有關(guān)作用域鏈的解析,請(qǐng)看我的這一篇文章。
有關(guān)this指針是如何確定的,請(qǐng)看這一篇文章。