原文地址:深入理解閉包(二)——變量對象
執(zhí)行一個函數(shù)之前,JavaScript引擎會進(jìn)行準(zhǔn)備工作,這個準(zhǔn)備工作指的就是執(zhí)行上下文,也叫執(zhí)行上下文環(huán)境,也叫執(zhí)行環(huán)境。
下面是一個執(zhí)行上下文的周期圖,其中變量對象(Variable Object)是重點之一,只有理解了它,我們才能知道一段代碼的執(zhí)行過程中先做什么,后做什么,我們今天就探討一下變量對象在執(zhí)行上下文生命周期中都經(jīng)歷了怎樣的變化:
創(chuàng)建階段
- 建立arguments對象(參數(shù)對象,當(dāng)某個函數(shù)接收參數(shù)的時候,會將參數(shù)封裝成arguments對象)。
- 檢查當(dāng)前上下文的函數(shù)聲明,也就是使用function聲明的函數(shù)。在變量對象中以函數(shù)名建立一個屬性,屬性值為指向該函數(shù)所在的內(nèi)存地址的引用。如果函數(shù)名的屬性已存在,那么該屬性將會被新的引用所覆蓋。
- 檢查當(dāng)前上下文的變量聲明,也就是使用var聲明的變量,對于每一個變量聲明,變量對象會以變量名建立一個屬性,屬性值為undefined。函數(shù)的聲明比變量優(yōu)先級要高,并且定義過程不會被變量覆蓋,除非是賦值。
執(zhí)行階段
執(zhí)行階段沒有什么難以理解的,有兩點需要知道一下:
- 代碼執(zhí)行時按照順序,該賦值時賦值,該調(diào)用時調(diào)用,這個一看后面的栗子就明白了。
- 變量對象轉(zhuǎn)變?yōu)榱嘶顒訉ο螅ˋctive Object),其實這倆是一個東西,只是所處的時期不同罷了。
栗子們
栗子1:
console.log(a); //a is not defined
上面代碼執(zhí)行環(huán)境中根本沒有定義變量a
console.log(a); //undefined
var a = 10;
雖然同樣沒有打印a的值,但是跟之前不同的是,這里執(zhí)行環(huán)境中查找到了變量a,但是在打印時它還沒有進(jìn)行賦值,因為查找變量a是在執(zhí)行上下文創(chuàng)建階段完成的,并定義為undefined,然后才進(jìn)入執(zhí)行階段,但是打印a的語句在前面,賦值語句在后面,所以打印出來的是a的初始值undefined,這也就體現(xiàn)了前面說的執(zhí)行階段是按照順序執(zhí)行相關(guān)賦值、打印、函數(shù)調(diào)用等代碼。我們可以把上面代碼等效成下面的形式:
var a;
console.log(a);
a = 10;
栗子2:
console.log(test); //function test(){ return 1;}
console.log(test()); //1 test是函數(shù),test()是函數(shù)調(diào)用之后返回的值
function test() {
return 1;
}
上面代碼在創(chuàng)建階段檢查到的是函數(shù)聲明,因此可以順利打印函數(shù)。
console.log(test); //undefined
console.log(test()); //test is not a function
var test = function () {
return 1;
}
這段代碼也是要打印函數(shù),但是卻得到了截然不同的效果,因為這里的test其實是一個變量聲明,只不過給它賦值的是一個函數(shù),但是在創(chuàng)建階段它的初始值仍是undefined。
栗子3:
function test(x,y) {
console.log(arguments); //{ '0': 1, '1': 2 }
console.log(a); //undefined
console.log(b); //function func(){ return 2; }
console.log(b()); //2
console.log(c); //unundefined
var a = 10;
console.log(a); //10
function b() {
return 1;
}
function b() {
return 2;
}
var b;
var c=function () {
return 3;
}
console.log(c); //function func(){ return 2; }
console.log(c()); //3
}
test(1,2);
- 檢查到函數(shù)調(diào)用時傳入了參數(shù)(1,2),以參數(shù)為屬性值封裝成arguments對象{ '0': 1, '1': 2 }。
- 檢查函數(shù)聲明,先檢查到第一個b函數(shù),便以b為屬性名,b函數(shù)的內(nèi)存地址引用為屬性值建立一個屬性。但緊接著又檢查到一個b函數(shù),屬性值將會被后面的b函數(shù)覆蓋,因此打印b函數(shù)時得到的是后面聲明的b函數(shù)。
- 檢查變量聲明,得到變量a,b,c,由于函數(shù)b的存在,b屬性已經(jīng)被創(chuàng)建了,并且這里的變量b并沒有賦值,因此它不會覆蓋屬性b的值,而是跳過。不過另外的變量a和c都順利創(chuàng)建屬性a和c,屬性值為undefined。
- 按順序執(zhí)行代碼,第一次打印a時還沒有經(jīng)過賦值,因此輸出undefined,第二次打印a時已經(jīng)賦值,因此輸出10,其他同理,不再解釋。
如果按照執(zhí)行順序把栗子3代碼做個等效的話,是下面這樣的:
function test(x,y) {
function b() {
return 2;
}
var a;
var c;
console.log(arguments);
console.log(a);
console.log(b);
console.log(b());
console.log(c);
a = 10;
console.log(a);
c=function () {
return 3;
}
console.log(c);
console.log(c());
}
test(1,2);
結(jié)語
現(xiàn)在我猜你對于一段代碼的執(zhí)行過程應(yīng)該已經(jīng)了解得差不多了,你不要覺得這跟閉包看起來沒有關(guān)系就忽略這里的知識點,其實你對基礎(chǔ)的東西理解透徹,對后面難點的理解是潛移默化的,不僅僅是閉包,很多難點在夯實的基礎(chǔ)面前都是赤身裸體的,很容易就能看得一清二楚。今天關(guān)于變量對象就說到這里,下一次我們會進(jìn)一步探查執(zhí)行上下文的奧秘。