執(zhí)行上下文
可執(zhí)行代碼
JavaScript中可執(zhí)行代碼(executable code)分為三種:全局代碼袭厂,函數(shù)代碼和eval代碼。
可執(zhí)行代碼與執(zhí)行上下文的概念是相對的,在某些語義下拓春,可執(zhí)行代碼與執(zhí)行上下文是等價的。
執(zhí)行上下文
執(zhí)行上下文(Execution Context亚隅,EC)硼莽,也稱執(zhí)行環(huán)境,每當(dāng)控制器執(zhí)行到可執(zhí)行代碼的時煮纵,就會進入到一個執(zhí)行上下文懂鸵。
執(zhí)行上下文可以理解為可執(zhí)行代碼的"運行環(huán)境",分為三種:
全局環(huán)境:當(dāng)一段程序開始執(zhí)行時行疏,會首先進入全局執(zhí)行文環(huán)境匆光,瀏覽器中是window對象,只有沒有關(guān)閉瀏覽器酿联,一直存在殴穴。
函數(shù)環(huán)境:每當(dāng)函數(shù)被調(diào)用執(zhí)行時,就會進入一個新的上下文環(huán)境。(函數(shù)遞歸調(diào)用也會進入)
eval環(huán)境:eval函數(shù)調(diào)用的時候產(chǎn)生的執(zhí)行上下文采幌。
執(zhí)行上下文也可以抽象理解為一個對象劲够,這個對象都有三個屬性:
變量對象(variable object)、作用域鏈(scope chain)休傍、this指針(this value)征绎。
不同執(zhí)行上下文變量對象略有不同:
全局上下文中的變量對象就是全局對象,允許通過變量對象的屬性名來間接訪問磨取。
函數(shù)上下文中用活動對象來表示變量對象人柿,通過函數(shù)的arguments屬性初始化。
執(zhí)行上下文生命周期分為兩個階段:創(chuàng)建階段和代碼執(zhí)行階段
創(chuàng)建階段:創(chuàng)建變量對象忙厌,建立作用域鏈凫岖,確定this指向
代碼執(zhí)行階段:變量賦值,函數(shù)引用逢净,執(zhí)行其他代碼
執(zhí)行上下文棧
JavaScript引擎通過執(zhí)行上下文棧(Execution Context Stack)來管理執(zhí)行上下文哥放。
當(dāng)一個執(zhí)行上下文(caller)激活了另一個上下文(callee),這個caller就會暫停它自身的執(zhí)行爹土,將控制權(quán)交給callee甥雕,于是callee被壓入棧頂,稱為當(dāng)前上下文胀茵。當(dāng)這個callee的上下文結(jié)束之后被彈出社露,然后caller從暫停的地方繼續(xù)執(zhí)行。一個callee可以用返回(return)或拋出異常(exception)來結(jié)束自身的上下文琼娘。
ECStack底部永遠都是全局上下文(global EC)峭弟,而頂部就是當(dāng)前(激活的)執(zhí)行上下文(active EC)。
// demo1
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function foo(){
return scope;
}
return foo();
}
checkscope();
// 1.首先向執(zhí)行上下文棧中壓入全局執(zhí)上下文
ECStack = [globalContext];
// 2.調(diào)用checkscope()脱拼,checkscope執(zhí)行上下文被壓入ECStack
ECStack.push[checkscopeContext] = [
checkscopeContext,
globalContext
];
// 3.調(diào)用foo()瞒瘸,foo執(zhí)行上下文被壓入ECStack
ECStack.push[fooContext] = [
fooContext,
checkscopeContext,
globalContext
];
// 4.foo()執(zhí)行結(jié)束,foo執(zhí)行上下文被彈出ECStack
ECStack.pop[fooContext] = [
checkscopeContext,
globalContext
];
// 5.checkscope()執(zhí)行結(jié)束挪拟,checkscope執(zhí)行上下文被彈出ECStack
ECStack.pop[checkscopeContext] = [globalContext];
// demo2
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function foo(){
return scope;
}
return foo;
}
checkscope()();
// 1.首先向執(zhí)行上下文棧中壓入全局執(zhí)上下文
ECStack = [globalContext];
// 2.調(diào)用checkscope()挨务,checkscope執(zhí)行上下文被壓入ECStack
ECStack.push[checkscopeContext] = [
checkscopeContext,
globalContext
];
// 3.checkscope()執(zhí)行結(jié)束后返回foo函數(shù)體,checkscope執(zhí)行上下文被彈出ECStack
ECStack.pop[checkscopeContext] = [globalContext];
// 4.foo函數(shù)體被返回后調(diào)用執(zhí)行玉组,foo執(zhí)行上下文被壓入ECStack
ECStack.push[fooContext] = [
fooContext,
globalContext
];
// 5.foo()執(zhí)行結(jié)束谎柄,foo執(zhí)行上下文被彈出ECStack
ECStack.pop[fooContext] = [globalContext];
變量對象
變量對象(variable object,VO)是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域惯雳,用于存儲定義在上下文中的變量和函數(shù)聲明朝巫。
函數(shù)表達式不包含在變量對象中。
var num = 10; // 變量聲明
function fun() {} // 函數(shù)聲明, FD
(function bar() {}); // 函數(shù)表達式, FE
console.log(
this.num === num, // true
window.fun === fun // true
);
console.log(bar); // "bar" is not defined
// globalContext.VO = {
fun: <fun reference>, // 函數(shù)fun的引用地址
num: undefined // 變量num石景,賦值為undefined
}
在全局上下文中劈猿,變量對象就是全局對象(在瀏覽器中是window對象)拙吉,允許通過全局對象的屬性名來訪問全局變量。
在函數(shù)執(zhí)行上下文中揪荣,用活動對象(active object筷黔,AO)來表示變量對象,AO不能直接訪問仗颈。
活動對象除了變量和函數(shù)聲明佛舱,還存儲了形參和arguments對象。
arguments對象是對形參的一個映射挨决,但是值是通過索引來獲取(類數(shù)組)请祖。
function foo(x, y) {
var z = 30;
function bar() {} // FD
(function baz() {}); // FE
}
foo(10, 20);
//fooContext.AO = {
arguments: {
0: 10,
1: 20,
length: 2
},
bar: <bar reference>,
x: 10,
y: 20,
z: 30
}
在進入執(zhí)行環(huán)境時,變量對象會進行如下初始化:
(1)arguments對象脖祈,對象中的值被賦予具體的實參值肆捕。
(2)函數(shù)的形參:創(chuàng)建一個屬性,其屬性名為形參名盖高,其值為實參的值慎陵;對于沒有傳遞的參數(shù),其值為undefined或舞。
(3)函數(shù)聲明:創(chuàng)建一個屬性荆姆,其屬性名和值都是函數(shù)對象創(chuàng)建出來的蒙幻,其值為指向某個函數(shù)對象的引用映凳;如果該函數(shù)名的屬性已存在,則該屬性會被新的引用覆蓋邮破。
(4)變量聲明:創(chuàng)建一個屬性诈豌,其屬性名即為變量名,其值為undefined抒和。如果變量名與已聲明的形參或函數(shù)名相同矫渔,則會直接跳過,原屬性值不會被修改摧莽。變量只能使用var關(guān)鍵字聲明庙洼,不使用var關(guān)鍵字的賦值語句僅僅是給全局對象創(chuàng)建了一個新屬性,不是變量镊辕。
總結(jié):在進入執(zhí)行上下文的時候(比如進入全局環(huán)境或者某個函數(shù)被調(diào)用)油够,變量對象除了arguments、函數(shù)的聲明以及參數(shù)被賦予了具體的屬性值征懈,其它的變量屬性默認的都是undefined石咬。
在執(zhí)行到函數(shù)內(nèi)部的具體某個語句的時候,上面所述的值為undefined的變量卖哎,其值都會被賦予具體的值鬼悠。
function test() {
var a = 1;
function foo() {
return 2;
}
var bar = function () {
return 'hello';
};
}
test();
// test()調(diào)用時進入該函數(shù)執(zhí)行上下文
testContext = [
VO: {}, // 創(chuàng)建變量對象
Scope: {}, // 建立作用域鏈
this:{} // 確定this指向
]
// 創(chuàng)建變量對象VO删性,里面的屬性不能被訪問
AO = {
arguments: {length: 0}, // 初始化Arguments對象
foo: <foo reference>, // 函數(shù)foo的引用地址
a: undefined // 變量a,賦值為undefined
bar: undefined // 變量bar焕窝,值為undefined
}
// 執(zhí)行階段
AO = {
arguments: {length: 0},
foo: <foo reference>,
a: 1 // 變量a蹬挺,賦值該為1
bar: <bar reference> // 匿名函數(shù)的引用地址賦值給bar
}
每進入到一個執(zhí)行環(huán)境都會創(chuàng)建一個變量對象,這個對象中記錄了在當(dāng)前執(zhí)行環(huán)境中可以訪問到的變量和函數(shù)它掂,它們以變量對象的屬性形式存在汗侵。也就是說這個變量對象成為“作用域”這個抽象概念的實體。
參考資料:
《JavaScript高級程序設(shè)計》
《JavaScript 標準參考教程》
湯姆大叔-深入理解JavaScript系列