1.一些常見術(shù)語
- AO:函數(shù)執(zhí)行前產(chǎn)生的一個對象
- GO:全局對象
- VO:變量對象库北,一般指向AO或GO
- ECS:上下文執(zhí)行棧
- GECS:全局上下文執(zhí)行棧
- FECS:函數(shù)上下文執(zhí)行棧
2.全局代碼的執(zhí)行過程
這是一段全局代碼
var name="why";
console.log(num1);
var num1=20;
var num2=30;
var result=num1+num2
console.log(result);
在代碼編譯之前渔隶,會創(chuàng)建一個GlobalObject對象鞍爱,這個對象包括一些全局的其他對象历等,并創(chuàng)建一個window屬性指向這個對象本身。
在全局代碼編譯的時候挣棕,遇到普通變量會放入到GlobalObject這個對象中宝泵,賦值為undefined
-
全局代碼執(zhí)行時丛肮,會先創(chuàng)建一個Global Execution Stack 全局執(zhí)行棧墙牌,里面包括兩個部分
- Variable Object:在全局執(zhí)行棧中涡驮,這個對象是指向的GlobalObject
- 執(zhí)行代碼
執(zhí)行的代碼,遇到變量要取值時喜滨,會先從VO找對應(yīng)的值捉捅,從上往下依次執(zhí)行
第二行輸出時,要取num1的值虽风,去VO找的時候棒口,因為還沒執(zhí)行到后面,所以此時的值是undefined
3. 全局代碼的執(zhí)行過程(函數(shù))
函數(shù)和普通變量是不一樣辜膝,如果在聲明之前提前調(diào)用无牵,還是會正常執(zhí)行結(jié)果,而普通變量則是undefined
var name="why";
foo()
function foo(){
console.log(m);
var m=10;
var n=20;
console.log("foo");
}
在編譯代碼之前内舟,會創(chuàng)建一個全局GO對象合敦,放入相關(guān)屬性
-
在編譯代碼的時候,將name放入到GO對象中验游,設(shè)置其值為undefined。然后將foo放入到GO中保檐,發(fā)現(xiàn)foo是一個函數(shù)耕蝉,所以會在內(nèi)存中開辟一片空間,來保存這個函數(shù)夜只,這個內(nèi)存空間包含兩個部分垒在,一個是父級作用域,一個是函數(shù)執(zhí)行體扔亥。然后將GO.foo的值設(shè)置為這片空間的內(nèi)存地址场躯。
var GO={ name:undefined, foo:'0x001' }
-
在執(zhí)行全局代碼的時候,會創(chuàng)建一個GES全局執(zhí)行棧旅挤,然后將GES放入到ECS上下文執(zhí)行棧中
- GES包括兩個部分
- VO(Variable Object):此時它指向的是GO
- 執(zhí)行代碼
- GES包括兩個部分
執(zhí)行全局代碼踢关,執(zhí)行第一行時,將VO.name的值設(shè)置 為"why"
-
執(zhí)行第二行時粘茄,去取foo的值签舞,然后從VO中找秕脓,返回一個內(nèi)存地址。但是發(fā)現(xiàn)這個foo函數(shù)會執(zhí)行儒搭。函數(shù)執(zhí)行的時候會自動創(chuàng)建一個函數(shù)上下文執(zhí)行棧FES(Functional Execution Stack),FES包括兩個部分
-
VO(Variable Object):AO(Activation Object)
-
AO:在函數(shù)編譯前會創(chuàng)建一個AO對象吠架,它在編譯的時候,會將m搂鲫、n放入到這個對象內(nèi)部傍药,然后設(shè)置其值為undefined
var AO={ m:undefined, n:undefined }
-
執(zhí)行代碼
-
-
在執(zhí)行foo函數(shù)的時候,先輸出m,從VO中查找,輸出undefined魂仍,然后會將m怔檩、n變量設(shè)置為具體的值
var AO={ m:10, n:20 }
執(zhí)行完foo函數(shù)后,這個函數(shù)上下文執(zhí)行棧會移出上下文棧蓄诽,然后銷毀薛训,如果AO對象沒有任何引用的話,后面也會被銷毀
4. 作用域鏈
var name="why";
foo()
function foo(){
console.log(m);
var m=10;
var n=20;
console.log(name);
}
當我們查找變量時仑氛,是沿著作用域鏈進行查找的乙埃。所以輸出的值是"why"
因為函數(shù)會存在嵌套,如果還是沒找到锯岖,會繼續(xù)往上一層進行查找介袜,一層一層往上找,直到全局作用域中出吹,如果還是沒有找到遇伞,則會報錯。
上面在VO查找的時候捶牢,發(fā)現(xiàn)沒有name鸠珠,然后再往父級作用域中查找GO,發(fā)現(xiàn)有name秋麸,則返回對應(yīng)name對應(yīng)的值
其實函數(shù)執(zhí)行棧包含的兩部分
- 第一部分不僅僅包含 VO渐排,還包含作用域鏈:這個作用域鏈是由當前的VO和ParentScope
- 父級作用域其實在編譯的時候就已經(jīng)確定好的,所以foo父級作用域是GO
- 所以作用域鏈是VO+GO
5. 全局代碼執(zhí)行的過程(函數(shù)嵌套)
var name="why";
foo()
function foo(){
console.log(m);
var m=10;
var n=20;
function bar(){
console.log(name);
}
bar()
}
如果一個函數(shù)嵌套另外一個函數(shù)灸蟆,另外一個函數(shù)剛開始是不需要被執(zhí)行的時候驯耻,是不會被編譯的,只會被預(yù)編譯炒考,
-
例如bar函數(shù)執(zhí)行時可缚,也會自動創(chuàng)建函數(shù)上下文執(zhí)行棧FES,里面包含兩個部分:
- 第一部分
- VO:AO
- AO:arguments
- scope-chain:VO+ParentSope
- this:是在運行時進行綁定的
- VO:AO
- 第二部分
- 執(zhí)行代碼
- 第一部分
執(zhí)行第8行代碼的時候,輸出name的值斋枢,會先從當前的VO中查找name帘靡,發(fā)現(xiàn)不存在,去父級作用域(foo)的VO中查找杏慰,發(fā)現(xiàn)還是不存在测柠,就是去父級作用域(foo)的父級作用域(GO)中去查找,發(fā)現(xiàn)有name的值炼鞠,是"why",所以將其輸出"why"轰胁。
6. 函數(shù)調(diào)用函數(shù)的執(zhí)行過程
var message="hello Global";
function foo(){
console.log(message);
}
function bar(){
var message="hello bar";
foo();
}
bar();
打印的結(jié)果是 hello Global
- 函數(shù)的作用域是在編譯的時候就已經(jīng)確定了
- 函數(shù)的作用域跟它定義的位置有關(guān)系谒主,跟調(diào)用的位置是沒有關(guān)系的。
7. 變量環(huán)境(Variable Enviroment)和環(huán)境記錄(Environment Record)
其實上面的講解是基于早期ECMA的版本規(guī)范:
- VO赃阀、GO霎肯、AO 這是ECMAScript5以前的規(guī)范
- 每個執(zhí)行上下文(GEC、FEC)會被關(guān)聯(lián)到一個變量對象中(Variable Object)榛斯,在源代碼中聲明的變量和聲明的函數(shù)都會作為屬性放入到VO中观游。對于函數(shù)來說,參數(shù)也會放入到VO中驮俗。
在最新的ECMASCript規(guī)范中懂缕,對于一些詞匯作了一些修改:
- 變量環(huán)境(VE)和環(huán)境記錄(ER)(不一定用對象實現(xiàn),也可以用map實現(xiàn))
- 每個執(zhí)行上下文都會被關(guān)聯(lián)到一個變量環(huán)境中王凑,在執(zhí)行代碼中搪柑,聲明的變量和聲明的函數(shù)都會作為環(huán)境記錄添加到變量環(huán)境中
- 對于函數(shù)而言,參數(shù)也會作為環(huán)境記錄加入到變量環(huán)境中索烹。
通過上面的變化工碾,我們可以知道,VO變?yōu)樽兞凯h(huán)境(VE)
8.作用域提升面試題
8.1 面試題1
var n=100;
function foo(){
n=200;
}
foo()
console.log(n);//200
8.2 面試題2
function foo(){
console.log(n);//undefined
var n=200;
console.log(n); //200
}
var n=100;
foo()
輸出 undefined 200
8.3 面試題3
var n=100;
function foo1(){
console.log(n);
}
function foo2(){
var n=200;
console.log(n);
foo1()
}
foo2();
console.log(n);
輸出 200 100 100
8.4 面試題4
var a=100;
function foo(){
console.log(a);
return
var a=100;
}
foo()
輸出undefined
8.5 面試題5
function foo(){
var m=200;
}
foo()
console.log(m);
會報錯百姓,報m找不到
-
執(zhí)行全局代碼前渊额,會先創(chuàng)建一個GO對象,對代碼進行編譯的時候垒拢,會將聲明的變量和聲明的函數(shù)作為屬性加入到GO中旬迹。
var GO={ foo:'0X001' }
-
在執(zhí)行全局代碼時,會創(chuàng)建一個全局執(zhí)行上下文棧VO指向GO子库,執(zhí)行第4行的時候舱权,執(zhí)行foo函數(shù)。執(zhí)行函數(shù)的時候會創(chuàng)建一個函數(shù)執(zhí)行上下文(FECS)仑嗅。FECS的VO執(zhí)行AO會先編譯foo函數(shù),將foo函數(shù)中聲明的變量和聲明的函數(shù)添加到FECS的AO對象中
var AO={ m:undefined }
執(zhí)行FECS中代碼時张症,將AO.m賦值為200.執(zhí)行完foo函數(shù)完后仓技,F(xiàn)ECS也移除上下文執(zhí)行棧,AO對象沒有被引用也隨之銷毀
執(zhí)行第5行時俗他,會去GO中查找m發(fā)現(xiàn)沒有脖捻,則會報一個錯誤 m is not defined
8.6 面試題6(特殊語法)
function foo(){
m=200;
}
foo()
console.log(m);
- 嚴格模式下,會報錯
- 非嚴格模式下兆衅,輸出 200
如果在函數(shù)中沒有聲明 某個變量地沮,但是卻去賦值了嗜浮。它這個變量會先被定義到全局對象中,然后再去執(zhí)行賦值操作
8.7 面試題7
function foo(){
var a=b=100;
}
foo()
console.log(a);
console.log(b);
輸出a的時候會報錯摩疑,因為a未在GO中聲明
輸出b的時候不會報錯危融,輸出的是100