通過(guò)實(shí)例了解JS執(zhí)行上下文運(yùn)行原理
壹 ❀ 引
我們都知道,JS代碼的執(zhí)行順序總是與代碼先后順序有所差異,當(dāng)先拋開(kāi)異步問(wèn)題你會(huì)發(fā)現(xiàn)就算是同步代碼,它的執(zhí)行也與你的預(yù)期不一致,比如:
function f1() { console.log('聽(tīng)風(fēng)是風(fēng)'); }; f1(); //echo function f1() { console.log('echo'); }; f1(); //echo
按照代碼書寫順序,應(yīng)該先輸出 聽(tīng)風(fēng)是風(fēng),再輸出 echo才對(duì),很遺憾,兩次輸出均為 echo;如果我們將上述代碼中的函數(shù)聲明改為函數(shù)表達(dá)式,結(jié)果又不太一樣:
var f1 = function () { console.log('聽(tīng)風(fēng)是風(fēng)'); }; f1(); //聽(tīng)風(fēng)是風(fēng) var f1 = function() { console.log('echo'); }; f1(); //echo
這說(shuō)明代碼在執(zhí)行前一定發(fā)生了某些微妙的變化,JS引擎究竟做了什么呢?這就不得不提JS執(zhí)行上下文的了。
貳 ❀ JS執(zhí)行上下文
JS代碼在執(zhí)行前,JS引擎總要做一番準(zhǔn)備工作,這份工作其實(shí)就是創(chuàng)建對(duì)應(yīng)的執(zhí)行上下文;
執(zhí)行上下文有且只有三類,全局執(zhí)行上下文,函數(shù)上下文,與eval上下文;由于eval一般不會(huì)使用,這里不做討論。
1.全局執(zhí)行上下文
全局執(zhí)行上下文只有一個(gè),在客戶端中一般由瀏覽器創(chuàng)建,也就是我們熟知的window對(duì)象,我們能通過(guò)this直接訪問(wèn)到它。
全局對(duì)象window上預(yù)定義了大量的方法和屬性,我們?cè)谌汁h(huán)境的任意處都能直接訪問(wèn)這些屬性方法,同時(shí)window對(duì)象還是var聲明的全局變量的載體。我們通過(guò)var創(chuàng)建的全局對(duì)象,都可以通過(guò)window直接訪問(wèn)。
2.函數(shù)執(zhí)行上下文
函數(shù)執(zhí)行上下文可存在無(wú)數(shù)個(gè),每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)都會(huì)創(chuàng)建一個(gè)函數(shù)上下文;需要注意的是,同一個(gè)函數(shù)被多次調(diào)用,都會(huì)創(chuàng)建一個(gè)新的上下文。
說(shuō)到這你是否會(huì)想,上下文種類不同,而且創(chuàng)建的數(shù)量還這么多,它們之間的關(guān)系是怎么樣的,又是誰(shuí)來(lái)管理這些上下文呢,這就不得不說(shuō)說(shuō)執(zhí)行上下文棧了。
叁 ❀ 執(zhí)行上下文棧(執(zhí)行棧)
執(zhí)行上下文棧(下文簡(jiǎn)稱執(zhí)行棧)也叫調(diào)用棧,執(zhí)行棧用于存儲(chǔ)代碼執(zhí)行期間創(chuàng)建的所有上下文,具有LIFO(Last In First Out后進(jìn)先出,也就是先進(jìn)后出)的特性。
JS代碼首次運(yùn)行,都會(huì)先創(chuàng)建一個(gè)全局執(zhí)行上下文并壓入到執(zhí)行棧中,之后每當(dāng)有函數(shù)被調(diào)用,都會(huì)創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文并壓入棧內(nèi);由于執(zhí)行棧LIFO的特性,所以可以理解為,JS代碼執(zhí)行完畢前在執(zhí)行棧底部永遠(yuǎn)有個(gè)全局執(zhí)行上下文。
function f1() { f2(); console.log(1); }; function f2() { f3(); console.log(2); }; function f3() { console.log(3); }; f1();//3 2 1
我們通過(guò)執(zhí)行棧與上下文的關(guān)系來(lái)解釋上述代碼的執(zhí)行過(guò)程,為了方便理解,我們假象執(zhí)行棧是一個(gè)數(shù)組,在代碼執(zhí)行初期一定會(huì)創(chuàng)建全局執(zhí)行上下文并壓入棧,因此過(guò)程大致如下:
//代碼執(zhí)行前創(chuàng)建全局執(zhí)行上下文 ECStack = [globalContext]; // f1調(diào)用 ECStack.push('f1 functionContext'); // f1又調(diào)用了f2,f2執(zhí)行完畢之前無(wú)法console 1 ECStack.push('f2 functionContext'); // f2又調(diào)用了f3,f3執(zhí)行完畢之前無(wú)法console 2 ECStack.push('f3 functionContext'); // f3執(zhí)行完畢,輸出3并出棧 ECStack.pop(); // f2執(zhí)行完畢,輸出2并出棧 ECStack.pop(); // f1執(zhí)行完畢,輸出1并出棧 ECStack.pop(); // 此時(shí)執(zhí)行棧中只剩下一個(gè)全局執(zhí)行上下文
那么到這里,我們解釋了執(zhí)行棧與執(zhí)行上下文的存儲(chǔ)規(guī)則;還記得我在前文提到代碼執(zhí)行前JS引擎會(huì)做準(zhǔn)備創(chuàng)建執(zhí)行上下文嗎,具體怎么創(chuàng)建呢,我們接著說(shuō)。
肆 ❀ 執(zhí)行上下文創(chuàng)建階段
執(zhí)行上下文創(chuàng)建分為創(chuàng)建階段與執(zhí)行階段兩個(gè)階段,較為難理解應(yīng)該是創(chuàng)建階段,我們先說(shuō)創(chuàng)建階段。
JS執(zhí)行上下文的創(chuàng)建階段主要負(fù)責(zé)三件事:確定this---創(chuàng)建詞法環(huán)境組件(LexicalEnvironment)---創(chuàng)建變量環(huán)境組件(VariableEnvironment)
這里我就直接借鑒了他人翻譯資料的偽代碼,來(lái)表示這個(gè)創(chuàng)建過(guò)程:
ExecutionContext = { // 確定this的值 ThisBinding = <this value>, // 創(chuàng)建詞法環(huán)境組件 LexicalEnvironment = {}, // 創(chuàng)建變量環(huán)境組件 VariableEnvironment = {}, };
如果你有閱讀其它關(guān)于執(zhí)行上下文的文章讀到這里一定有疑問(wèn),執(zhí)行上下文創(chuàng)建過(guò)程不是應(yīng)該解釋this,作用域與變量對(duì)象/活動(dòng)對(duì)象才對(duì)嗎,怎么跟別的地方說(shuō)的不一樣,這點(diǎn)我后面解釋。
1.確定this
官方的稱呼為This Binding,在全局執(zhí)行上下文中,this總是指向全局對(duì)象,例如瀏覽器環(huán)境下this指向window對(duì)象。
而在函數(shù)執(zhí)行上下文中,this的值取決于函數(shù)的調(diào)用方式,如果被一個(gè)對(duì)象調(diào)用,那么this指向這個(gè)對(duì)象。否則this一般指向全局對(duì)象window或者undefined(嚴(yán)格模式)。
2.詞法環(huán)境組件
詞法環(huán)境是一個(gè)包含標(biāo)識(shí)符變量映射的結(jié)構(gòu),這里的標(biāo)識(shí)符表示變量/函數(shù)的名稱,變量是對(duì)實(shí)際對(duì)象【包括函數(shù)類型對(duì)象】或原始值的引用。
詞法環(huán)境由環(huán)境記錄與對(duì)外部環(huán)境引入記錄兩個(gè)部分組成。
其中環(huán)境記錄用于存儲(chǔ)當(dāng)前環(huán)境中的變量和函數(shù)聲明的實(shí)際位置;外部環(huán)境引入記錄很好理解,它用于保存自身環(huán)境可以訪問(wèn)的其它外部環(huán)境,那么說(shuō)到這個(gè),是不是有點(diǎn)作用域鏈的意思?
我們?cè)谇拔奶岬搅巳謭?zhí)行上下文與函數(shù)執(zhí)行上下文,所以這也導(dǎo)致了詞法環(huán)境分為全局詞法環(huán)境與函數(shù)詞法環(huán)境兩種。
全局詞法環(huán)境組件:
對(duì)外部環(huán)境的引入記錄為null,因?yàn)樗旧砭褪亲钔鈱迎h(huán)境,除此之外它還記錄了當(dāng)前環(huán)境下的所有屬性、方法位置。
函數(shù)詞法環(huán)境組件:
包含了用戶在函數(shù)中定義的所有屬性方法外,還包含了一個(gè)arguments對(duì)象。函數(shù)詞法環(huán)境的外部環(huán)境引入可以是全局環(huán)境,也可以是其它函數(shù)環(huán)境,這個(gè)根據(jù)實(shí)際代碼而來(lái)。
這里借用譯文中的偽代碼(環(huán)境記錄在全局和函數(shù)中也不同,全局中的環(huán)境記錄叫對(duì)象環(huán)境記錄,函數(shù)中環(huán)境記錄叫聲明性環(huán)境記錄,說(shuō)多了糊涂,下方有展示):
// 全局環(huán)境 GlobalExectionContext = { // 全局詞法環(huán)境 LexicalEnvironment: { // 環(huán)境記錄 EnvironmentRecord: { Type: "Object", //類型為對(duì)象環(huán)境記錄 // 標(biāo)識(shí)符綁定在這里 }, outer: < null > } }; // 函數(shù)環(huán)境 FunctionExectionContext = { // 函數(shù)詞法環(huán)境 LexicalEnvironment: { // 環(huán)境紀(jì)錄 EnvironmentRecord: { Type: "Declarative", //類型為聲明性環(huán)境記錄 // 標(biāo)識(shí)符綁定在這里 }, outer: < Global or outerfunction environment reference > } };
3.變量環(huán)境組件
變量環(huán)境可以說(shuō)也是詞法環(huán)境,它具備詞法環(huán)境所有屬性,一樣有環(huán)境記錄與外部環(huán)境引入。在ES6中唯一的區(qū)別在于詞法環(huán)境用于存儲(chǔ)函數(shù)聲明與let const聲明的變量,而變量環(huán)境僅僅存儲(chǔ)var聲明的變量。
我們通過(guò)一串偽代碼來(lái)理解它們:
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
我們用偽代碼來(lái)描述上述代碼中執(zhí)行上下文的創(chuàng)建過(guò)程:
//全局執(zhí)行上下文 GlobalExectionContext = { // this綁定為全局對(duì)象 ThisBinding: <Global Object>, // 詞法環(huán)境 LexicalEnvironment: { //環(huán)境記錄 EnvironmentRecord: { Type: "Object", // 對(duì)象環(huán)境記錄 // 標(biāo)識(shí)符綁定在這里 let const創(chuàng)建的變量a b在這 a: < uninitialized >, b: < uninitialized >, multiply: < func > } // 全局環(huán)境外部環(huán)境引入為null outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 對(duì)象環(huán)境記錄 // 標(biāo)識(shí)符綁定在這里 var創(chuàng)建的c在這 c: undefined, } // 全局環(huán)境外部環(huán)境引入為null outer: <null> } } // 函數(shù)執(zhí)行上下文 FunctionExectionContext = { //由于函數(shù)是默認(rèn)調(diào)用 this綁定同樣是全局對(duì)象 ThisBinding: <Global Object>, // 詞法環(huán)境 LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 聲明性環(huán)境記錄 // 標(biāo)識(shí)符綁定在這里 arguments對(duì)象在這 Arguments: {0: 20, 1: 30, length: 2}, }, // 外部環(huán)境引入記錄為</Global> outer: <GlobalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 聲明性環(huán)境記錄 // 標(biāo)識(shí)符綁定在這里 var創(chuàng)建的g在這 g: undefined }, // 外部環(huán)境引入記錄為</Global> outer: <GlobalEnvironment> } }
不知道你有沒(méi)有發(fā)現(xiàn),在執(zhí)行上下文創(chuàng)建階段,函數(shù)聲明與var聲明的變量在創(chuàng)建階段已經(jīng)被賦予了一個(gè)值,var聲明被設(shè)置為了undefined,函數(shù)被設(shè)置為了自身函數(shù),而let const被設(shè)置為未初始化。
現(xiàn)在你總知道變量提升與函數(shù)聲明提前是怎么回事了吧,以及為什么let const為什么有暫時(shí)性死域,這是因?yàn)樽饔糜騽?chuàng)建階段JS引擎對(duì)兩者初始化賦值不同。
上下文除了創(chuàng)建階段外,還有執(zhí)行階段,這點(diǎn)大家應(yīng)該好理解,代碼執(zhí)行時(shí)根據(jù)之前的環(huán)境記錄對(duì)應(yīng)賦值,比如早期var在創(chuàng)建階段為undefined,如果有值就對(duì)應(yīng)賦值,像let const值為未初始化,如果有值就賦值,無(wú)值則賦予undefined。
伍 ❀ 關(guān)于變量對(duì)象與活動(dòng)對(duì)象
回答前面的問(wèn)題,為什么別人的博文介紹上下文都是談作用域,變量對(duì)象和活動(dòng)對(duì)象,我這就成了詞法環(huán)境,變量環(huán)境了。
我在閱讀相關(guān)資料也產(chǎn)生了這個(gè)疑問(wèn),一番查閱可以確定的是,變量對(duì)象與活動(dòng)對(duì)象的概念是ES3提出的老概念,從ES5開(kāi)始就用詞法環(huán)境和變量環(huán)境替代了,因?yàn)楦媒忉尅?/p>
在上文中,我們通過(guò)介紹詞法環(huán)境與變量環(huán)境解釋了為什么var會(huì)存在變量提升,為什么let const沒(méi)有,而通過(guò)變量對(duì)象與活動(dòng)對(duì)象是很難解釋的,由其是在JavaScript在更新中不斷在彌補(bǔ)當(dāng)初設(shè)計(jì)的坑。
其次,詞法環(huán)境的概念與變量對(duì)象這類概念也是可以對(duì)應(yīng)上的。
我們知道變量對(duì)象與活動(dòng)對(duì)象其實(shí)都是變量對(duì)象,變量對(duì)象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲(chǔ)了在上下文中定義的變量和函數(shù)聲明。而在函數(shù)上下文中,我們用活動(dòng)對(duì)象(activation object, AO)來(lái)表示變量對(duì)象。
那這不正好對(duì)應(yīng)到了全局詞法記錄與函數(shù)詞法記錄了嗎。而且由于ES6新增的let const不存在變量提升,于是正好有了詞法環(huán)境與變量環(huán)境的概念來(lái)解釋這個(gè)問(wèn)題。
所以說(shuō)到這,你也不用為詞法環(huán)境,變量對(duì)象的概念鬧沖突了。
我們來(lái)總結(jié)下上面提到的概念。
陸 ❀ 總結(jié)
1.全局執(zhí)行上下文一般由瀏覽器創(chuàng)建,代碼執(zhí)行時(shí)就會(huì)創(chuàng)建;函數(shù)執(zhí)行上下文只有函數(shù)被調(diào)用時(shí)才會(huì)創(chuàng)建,調(diào)用多少次函數(shù)就會(huì)創(chuàng)建多少上下文。
2.調(diào)用棧用于存放所有執(zhí)行上下文,滿足FILO規(guī)則。
3.執(zhí)行上下文創(chuàng)建階段分為綁定this,創(chuàng)建詞法環(huán)境,變量環(huán)境三步,兩者區(qū)別在于詞法環(huán)境存放函數(shù)聲明與const let聲明的變量,而變量環(huán)境只存儲(chǔ)var聲明的變量。
4.詞法環(huán)境主要由環(huán)境記錄與外部環(huán)境引入記錄兩個(gè)部分組成,全局上下文與函數(shù)上下文的外部環(huán)境引入記錄不一樣,全局為null,函數(shù)為全局環(huán)境或者其它函數(shù)環(huán)境。環(huán)境記錄也不一樣,全局叫對(duì)象環(huán)境記錄,函數(shù)叫聲明性環(huán)境記錄。
5.你應(yīng)該明白了為什么會(huì)存在變量提升,函數(shù)提升,而let const沒(méi)有。
6.ES3之前的變量對(duì)象與活動(dòng)對(duì)象的概念在ES5之后由詞法環(huán)境,變量環(huán)境來(lái)解釋,兩者概念不沖突,后者理解更為通俗易懂。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解JavaScript數(shù)據(jù)類型和判斷方法
這篇文章主要介紹了JavaScript數(shù)據(jù)類型和判斷方法的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)JavaScript,感興趣的朋友可以了解下2020-09-09H5+C3+JS實(shí)現(xiàn)雙人對(duì)戰(zhàn)五子棋游戲(UI篇)
這篇文章主要為大家詳細(xì)介紹了H5+C3+JS實(shí)現(xiàn)雙人對(duì)戰(zhàn)五子棋游戲,實(shí)現(xiàn)雙人對(duì)戰(zhàn)模式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09js實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)的幾種方法小結(jié)
下面小編就為大家?guī)?lái)一篇js實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)的幾種方法小結(jié)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧2016-05-05JavaScript正則表達(dá)式校驗(yàn)與遞歸函數(shù)實(shí)際應(yīng)用實(shí)例解析
這篇文章主要介紹了JavaScript正則表達(dá)式校驗(yàn)與遞歸函數(shù)實(shí)際應(yīng)用,需要的朋友可以參考下2017-08-08