今天這篇文章,我們將了解 JavaScript 提供的黑盒,讓我們的代碼神奇地運(yùn)行“執(zhí)行上下文”。
這是迄今為止最重要的主題之一,它可以使你對(duì)其他關(guān)鍵主題一目了然,例如,作用域、詞法作用域、閉包和提升,而且學(xué)習(xí)JavaScript的真正工作原理很有趣。
到目前為止,在代碼編輯器(Vs code )中編寫的每一行混亂代碼都在我們現(xiàn)在將討論的這個(gè)執(zhí)行上下文中運(yùn)行。
坐下來(lái),放松一下,收拾好你的美食,因?yàn)槲視?huì)讓你明白的。
在 JavaScript 中,一切都發(fā)生在執(zhí)行上下文中,我的意思是一切。你可以將其視為評(píng)估和執(zhí)行 JavaScript 代碼的環(huán)境。
每當(dāng)你的瀏覽器與任何 JavaScript 代碼交叉路徑時(shí),瀏覽器的 JavaScript 引擎就會(huì)創(chuàng)建一個(gè)特殊的環(huán)境來(lái)處理此 JavaScript 代碼的轉(zhuǎn)換和執(zhí)行。這個(gè)環(huán)境被稱為執(zhí)行上下文。
執(zhí)行上下文包含當(dāng)前正在運(yùn)行的代碼以及有助于其執(zhí)行的所有內(nèi)容。
執(zhí)行上下文的類型
當(dāng)你在瀏覽器中運(yùn)行腳本時(shí),javascript 引擎會(huì)創(chuàng)建不同類型的執(zhí)行上下文。
全局執(zhí)行上下文 (GEC)
當(dāng)你第一次運(yùn)行腳本或你的代碼不在任何函數(shù)中時(shí),它會(huì)被放置在全局執(zhí)行上下文 (GEC) 中。
在這里,每當(dāng) JavaScript 引擎接收到一個(gè)腳本文件時(shí),它首先會(huì)創(chuàng)建一個(gè)默認(rèn)執(zhí)行上下文,這就是我們所說(shuō)的全局執(zhí)行上下文 (GEC)。它是一個(gè)基本/默認(rèn)執(zhí)行上下文,所有不在函數(shù)內(nèi)部的代碼都會(huì)在其中執(zhí)行。
注意:每個(gè) JavaScript文件只有一個(gè) GEC
函數(shù)執(zhí)行上下文 (FEC)
每當(dāng)你的 JavaScript 引擎遇到函數(shù)調(diào)用時(shí),它都會(huì)在全局執(zhí)行上下文中創(chuàng)建一種稱為函數(shù)執(zhí)行上下文的不同類型的 EC,以評(píng)估和執(zhí)行該函數(shù)內(nèi)部編寫的代碼。
每個(gè)函數(shù)調(diào)用都有自己的 FEC(即使你多次調(diào)用同一個(gè)函數(shù)),因此,在腳本運(yùn)行時(shí)可以有多個(gè) FEC。
它們是如何創(chuàng)建的?
現(xiàn)在執(zhí)行上下文的創(chuàng)建分兩個(gè)階段進(jìn)行:
創(chuàng)建階段
執(zhí)行階段
1、創(chuàng)建階段
在此階段,將創(chuàng)建一個(gè)執(zhí)行上下文對(duì)象 (ECO),其中包含我們的代碼在其運(yùn)行時(shí)(執(zhí)行階段)使用的重要信息/數(shù)據(jù)。
屬性在此對(duì)象 (ECO) 中分三個(gè)不同階段進(jìn)行設(shè)置和定義。
創(chuàng)建變量對(duì)象 (VO)。
創(chuàng)建范圍鏈。
賦予此關(guān)鍵字價(jià)值。
階段 1:變量對(duì)象的創(chuàng)建
變量對(duì)象就像一個(gè)在執(zhí)行上下文中創(chuàng)建的容器,它將變量和函數(shù)聲明存儲(chǔ)在鍵:值對(duì)(不是函數(shù)表達(dá)式)中。
在 GEC 中,使用 var 關(guān)鍵字聲明的每個(gè)變量都會(huì)向指向該變量的變量對(duì)象添加一個(gè)屬性,并將其值設(shè)置為未定義,使用 let 或 const 聲明的變量獲取未初始化的值,而在函數(shù)聲明中,一個(gè)屬性被添加到指向該函數(shù)的變量對(duì)象中,所有的函數(shù)聲明都將被存儲(chǔ)并可以在 VO 中訪問(wèn),甚至在代碼開(kāi)始運(yùn)行之前。
在 FEC 中,不會(huì)創(chuàng)建此變量對(duì)象,而是構(gòu)造了一個(gè)名為“argument”的類似數(shù)組的對(duì)象,其中包括提供給該函數(shù)的所有參數(shù)。
這種甚至在代碼執(zhí)行之前就將變量和函數(shù)(聲明)存儲(chǔ)在內(nèi)存中,這就是我們所說(shuō)的提升。
第 2 階段:創(chuàng)建范圍鏈
在 JavaScript 中,作用域是一種了解一段代碼對(duì)腳本其他域的可訪問(wèn)性的方法。
每個(gè)函數(shù)執(zhí)行上下文都會(huì)創(chuàng)建它的作用域,可以將其視為一個(gè)環(huán)境或空間,它定義的變量和函數(shù)可以通過(guò)一個(gè)稱為作用域的進(jìn)程來(lái)訪問(wèn)。現(xiàn)在,當(dāng)一個(gè)函數(shù)(比如 X() )在另一個(gè)函數(shù)(比如 Y() )中定義時(shí),這個(gè)內(nèi)部函數(shù) X() 將可以訪問(wèn)變量,并且在外部函數(shù) Y() 中定義的其他函數(shù)也將具有訪問(wèn)外部函數(shù)的代碼,但事情并不止于此,它還可以訪問(wèn)其父元素的代碼等等,直到 GCE,這種行為就是我們所說(shuō)的詞法作用域,但反過(guò)來(lái)不是真的。
這個(gè)作用域的概念在JavaScript 中引發(fā)了一個(gè)被稱為閉包的相關(guān)現(xiàn)象,即使在外部函數(shù)執(zhí)行完成之后,內(nèi)部函數(shù)也可以訪問(wèn)與外部函數(shù)關(guān)聯(lián)的代碼……它已經(jīng)死了,消失了,很久了走了。
讓我們?cè)倏匆粋€(gè)例子來(lái)理解作用域鏈。
這里變量 a 和 b 沒(méi)有在函數(shù) second() 中定義,它只能訪問(wèn)在其自己的范圍(本地范圍)中定義的變量 c,但是由于詞法范圍,它可以訪問(wèn)它所在的函數(shù)以及它的父母。因此,當(dāng)你運(yùn)行此代碼時(shí),JavaScript 引擎將無(wú)法找到變量 a 或 b,因此,它將沿著執(zhí)行上下文鏈?zhǔn)紫日业?b 并解析它,因?yàn)樗言诤瘮?shù) first() 范圍內(nèi)成功找到它,解析后繼續(xù)查找變量 a ,JavaScript 引擎為該變量一直到全局執(zhí)行上下文并解析它。
這個(gè) JavaScript 引擎沿著不同執(zhí)行上下文鏈向上的過(guò)程,或者我們可以說(shuō)遍歷執(zhí)行上下文的范圍以解析變量或函數(shù)調(diào)用/調(diào)用,稱為 Scope Chaining。
第 3 階段:設(shè)置“this”關(guān)鍵字的值
在 javascript 中,this 關(guān)鍵字是指執(zhí)行上下文所屬的范圍。
在 GEC 中,這指的是一個(gè)全局對(duì)象,在瀏覽器的情況下是一個(gè)窗口對(duì)象。因此,在函數(shù)聲明中,使用“var”關(guān)鍵字初始化的變量分別作為方法和屬性分配給這個(gè)全局對(duì)象。
所以
與以下內(nèi)容相同
但在 FEC 的情況下,它不會(huì)創(chuàng)建“this”關(guān)鍵字,而是可以訪問(wèn)定義它的環(huán)境的關(guān)鍵字。
執(zhí)行階段
在執(zhí)行上下文的這個(gè)階段,我們的代碼開(kāi)始執(zhí)行,執(zhí)行后從執(zhí)行堆棧或調(diào)用堆棧彈出,我們將在本文后面介紹。
到目前為止,Variable 對(duì)象包含值為 undefined 和 uninitialized 的變量,具體取決于變量是分別使用 var 關(guān)鍵字還是使用 let/const 聲明的。
這里 JavaScript引擎再次讀取 EC 中的代碼,用它們的實(shí)際值更新這些變量。然后代碼被解析,被轉(zhuǎn)譯,最后被執(zhí)行
執(zhí)行堆棧(調(diào)用堆棧)
你有沒(méi)有想過(guò) JavaScript 引擎如何跟蹤它在腳本運(yùn)行時(shí)創(chuàng)建的各種 EC 的所有這些創(chuàng)建和刪除?答案是執(zhí)行堆棧或簡(jiǎn)單的調(diào)用堆棧。
“JavaScript 是一種同步的單線程語(yǔ)言”
單線程是指它只能夠一次執(zhí)行一個(gè)任務(wù),一次是一行代碼,而同步是指這些任務(wù)的執(zhí)行以特定的順序發(fā)生。因此,當(dāng) JavaScript 引擎讀取腳本時(shí),它會(huì)創(chuàng)建不同的執(zhí)行上下文并將它們存儲(chǔ)在稱為調(diào)用堆棧或執(zhí)行堆棧的堆棧數(shù)據(jù)結(jié)構(gòu)中。
當(dāng)腳本在瀏覽器中加載時(shí),瀏覽器的 JS 引擎首先會(huì)創(chuàng)建一個(gè)我們?cè)谏厦嬖敿?xì)介紹過(guò)的默認(rèn)特殊環(huán)境,即全局執(zhí)行上下文,并將其推送到此執(zhí)行堆棧。
之后,當(dāng) JS 引擎發(fā)現(xiàn)函數(shù)調(diào)用時(shí)執(zhí)行文件時(shí),它會(huì)為其創(chuàng)建一個(gè)單獨(dú)的函數(shù)執(zhí)行上下文,如下圖所示(步驟 2),并將其推送到現(xiàn)有默認(rèn) GEC 之上的堆棧中。
在執(zhí)行 firstFunc() 時(shí),它遇到對(duì) secondFunc() 的調(diào)用,它暫停 firstFunc() 的執(zhí)行并創(chuàng)建另一個(gè) FEC 并推送到 firstFunc() FEC 頂部的堆棧,然后再次為 thirdFunc() 創(chuàng)建一個(gè)單獨(dú)的 FEC 稱呼。
頂部的 EC 將首先由 JS 引擎執(zhí)行,執(zhí)行完成后,它會(huì)從堆棧中彈出,并開(kāi)始執(zhí)行上一個(gè)活動(dòng) EC 下面的 EC,如上圖所示,直到到達(dá) GEC。
結(jié)論
執(zhí)行上下文是 JavaScript 的核心,理解它很重要,因?yàn)樗梢詭椭阏_理解其他主要概念。