詳解JavaScript中的執(zhí)行上下文
引言
當(dāng)我們在瀏覽器中運行JavaScript
代碼時,瀏覽器會先創(chuàng)建一個全局執(zhí)行上下文(Global Execution Context
),然后逐行解析和執(zhí)行代碼。
執(zhí)行上下文是JavaScript
中非常重要的概念,它決定了代碼的執(zhí)行順序和作用域鏈等重要信息。了解執(zhí)行上下文的概念和工作原理,對于理解JavaScript
的運行機制和調(diào)試錯誤非常有幫助。
在本文中,我們將深入探討JavaScript
的執(zhí)行上下文,從而幫助讀者更好地理解JavaScript
的運行機制。
1、什么是執(zhí)行上下文
一般來說,聽到上下文
這個東西,很自然想到了語文老師講到的在上下文中找到相關(guān)聯(lián)的段落和句子...
其實在JS
中的上下文更多的是一個抽象的概念。它具體是指在當(dāng)前執(zhí)行環(huán)境中的變量、函數(shù)聲明,參數(shù)(arguments),作用域鏈,this等信息。
1.1、瀏覽器如何理解執(zhí)行JavaScript
瀏覽器并不理解我們在應(yīng)用中編寫的高級JavaScript
代碼。代碼需要被轉(zhuǎn)換成瀏覽器和計算機能夠理解的格式——機器碼
。
瀏覽器在讀取HTML
時,如果遇到了<script>
標(biāo)簽或包含JavaScript
代碼的屬性如onClick
,會發(fā)送給JavaScript引擎
。
瀏覽器的JavaScript引擎
會創(chuàng)造一個特殊的環(huán)境來處理這些JavaScript
代碼的轉(zhuǎn)換和執(zhí)行。這個特殊的環(huán)境被稱為執(zhí)行上下文
。
執(zhí)行上下文包含當(dāng)前正在運行的代碼和有助于其執(zhí)行的所有內(nèi)容。在執(zhí)行上下文運行期間,編譯器
解析代碼,內(nèi)存存儲變量和函數(shù),可執(zhí)行的字節(jié)碼生成后,代碼執(zhí)行。
實在不好理解,先入為主,將之想象成一個執(zhí)行JS的容器
。
1.2、執(zhí)行上下文
執(zhí)行上下文
是JavaScript
中非常重要的概念,它代表了代碼執(zhí)行時的環(huán)境。每當(dāng)JavaScript引擎
執(zhí)行一段代碼時,都會創(chuàng)建一個執(zhí)行上下文。執(zhí)行上下文包含了三個重要的組成部分:變量對象
、作用域鏈
和this值
。
- 變量對象:是當(dāng)前執(zhí)行上下文中的變量、函數(shù)聲明和函數(shù)參數(shù)的存儲空間。
- 作用域鏈:是當(dāng)前執(zhí)行上下文中所有父級執(zhí)行上下文的變量對象的集合,它決定了當(dāng)前執(zhí)行上下文中變量的可訪問性。
- this值:代表當(dāng)前函數(shù)的執(zhí)行環(huán)境。
2、執(zhí)行上下文有哪些類型呢
JavaScript
中有三種執(zhí)行上下文類型
1.全局執(zhí)行上下文(GEC)
任何不在函數(shù)內(nèi)部的代碼都在全局上下文中。一個程序中只會有一個全局執(zhí)行上下文。
2.函數(shù)執(zhí)行上下文(FEC)
每當(dāng)一個函數(shù)被調(diào)用時, 都會為該函數(shù)創(chuàng)建一個新的上下文。每個函數(shù)都有它自己的執(zhí)行上下文,它在函數(shù)被調(diào)用時創(chuàng)建。函數(shù)上下文可以有任意多個。
3.Eval函數(shù)執(zhí)行上下文
執(zhí)行在eval
函數(shù)內(nèi)部的代碼也會有它屬于自己的執(zhí)行上下文。eval
不經(jīng)常被使用到。
小知識:
eval()
函數(shù)計算JavaScript
字符串,并把它作為腳本代碼來執(zhí)行。
如果參數(shù)是一個表達式,eval()
函數(shù)將執(zhí)行表達式。如果參數(shù)是Javascript
語句,eval()
將執(zhí)行Javascript
語句。
主要的還是全局執(zhí)行上下文和函數(shù)執(zhí)行上下文。
3、執(zhí)行上下文的生命周期
在JavaScript中,執(zhí)行上下文的生命周期可以分為三個階段:創(chuàng)建階段、執(zhí)行階段和銷毀階段。
3.1、創(chuàng)建階段
在創(chuàng)建階段,執(zhí)行上下文首先與執(zhí)行上下文對象(ECO)
相關(guān)聯(lián)。執(zhí)行上下文對象存儲了許多重要的數(shù)據(jù),執(zhí)行上下文中的代碼在運行時會使用這些數(shù)據(jù)。創(chuàng)建階段分三個步驟來定義和設(shè)置執(zhí)行上下文對象的屬性:
- 創(chuàng)建變量對象(
VO
) - 創(chuàng)建作用域鏈
- 設(shè)置
this
關(guān)鍵字的值
3.1.1、創(chuàng)建變量對象(VO)
變量對象(VO)
是一個在執(zhí)行上下文中創(chuàng)建的類似于對象的容器,存儲執(zhí)行上下文中變量
和函數(shù)聲明
。
在GEC
中,每當(dāng)使用var
關(guān)鍵字聲明變量,VO
就會添加一個指向該變量的屬性,并將值設(shè)置為undefined
。每當(dāng)函數(shù)聲明時,VO
就會添加一個指向該函數(shù)的屬性,并將這個屬性存儲在內(nèi)存中。這就意味著在開始運行代碼之前,所有函數(shù)聲明就已經(jīng)存儲在VO
中,并可以在VO
中訪問。
但在FEC
中并不創(chuàng)建VO
,而是生成一個類數(shù)組對象,稱為arguments對象
,在下文稱AO
,包含傳入函數(shù)的所有參數(shù)。
小知識:
這種將變量和函數(shù)聲明存儲在內(nèi)存中優(yōu)先于執(zhí)行代碼的過程被稱為提升。
3.1.2、創(chuàng)建作用域鏈
JavaScript
中的作用域鏈是一個機制,決定了一段代碼對于代碼庫中其他一些代碼來說的可訪問性。
可以帶著這樣一些問題思考:
- 一段代碼可以在哪里訪問?哪里不能訪問?
- 代碼哪些部分可以被訪問?哪些部分不能?
每一個函數(shù)執(zhí)行上下文都會創(chuàng)建一個作用域,作用域相當(dāng)于是一個空間/環(huán)境,變量和函數(shù)定義在這個空間里,并且可以通過一個叫做作用域查找的過程訪問。如果函數(shù)被定義在另一個函數(shù)內(nèi)部,處在內(nèi)部的函數(shù)可以訪問自己內(nèi)部的代碼以及外部函數(shù)(父函數(shù))的代碼。這種行為被稱作詞法作用域查找。但外部函數(shù)并不能訪問內(nèi)部函數(shù)的代碼。
小知識:
作用域的概念就引出了JavaScript
另一個相關(guān)的現(xiàn)象——閉包。閉包指的是內(nèi)部函數(shù)永遠可以訪問外部函數(shù)中的代碼,即便外部函數(shù)已經(jīng)執(zhí)行完畢。
JavaScript
引擎一路向上遍歷執(zhí)行上下文直至解析處在函數(shù)內(nèi)部觸發(fā)的變量和函數(shù)的概念就叫作用域鏈。
3.1.3、設(shè)置this關(guān)鍵字的值
JavaScript
中this
關(guān)鍵字指的是執(zhí)行上下文所屬的作用域。一旦作用域鏈被創(chuàng)建,JS引擎
就會初始化this
關(guān)鍵字的值。
全局上下文中的this值:
在GEC
(所有函數(shù)和對象之外)中,this
指向全局對象——window
對象。同時,由var
關(guān)鍵字初始化的函數(shù)聲明和變量會被作為全局對象(window
對象)的方法或者屬性。
在任何函數(shù)外聲明的變量和函數(shù),如下:
var name = "jack"; function getName() { console.log('hello') };
與下方的寫法是一致的:
window.name = "jack"; window.getName = () => { console.log('hello') };
在GEC
中的函數(shù)和變量會被當(dāng)作window
對象的方法和屬性。
函數(shù)中的this:
在FEC
中,并沒有創(chuàng)建this
對象,而是能夠訪問this
被定義的環(huán)境。
在函數(shù)內(nèi)部訪問this
的屬性,示例:
var msg = "hello world!"; function printMsg() { console.log(this.msg); } printMsg(); // hello world!
小知識:
在對象中,this
關(guān)鍵字并不指向GEC
,而是指向?qū)ο蟊旧怼?/p>
引用對象中的this
如同引用:
對象.定義在對象內(nèi)部的屬性或方法;
示例代碼:
var msg = "hello world!"; const Obj = { msg = "no hello world!"; printMsg() { console.log(this.msg); } } Obj.printMsg(); // no hello world!
出現(xiàn)上述的情況,函數(shù)可以訪問的this
關(guān)鍵字的值是定義其的對象Obj
,而不是全局對象。
this
關(guān)鍵字的值設(shè)置后,執(zhí)行上下文對象的所有屬性就定義完成,創(chuàng)建階段結(jié)束,JS引擎
就進入到執(zhí)行階段。
3.2、執(zhí)行階段
執(zhí)行上下文創(chuàng)建階段之后就是執(zhí)行階段了,在這一階段代碼執(zhí)行真正開始。創(chuàng)建階段之后,VO
包含的變量值為undefined
,如果在此時運行代碼,肯定會報錯,因此JavaScript
引擎無法執(zhí)行未定義的變量。
在執(zhí)行階段,JavaScript
引擎會再次讀取執(zhí)行上下文,并用變量的實際值更新VO
。編譯器再把代碼編譯為計算機可執(zhí)行的字節(jié)碼后執(zhí)行。如果在代碼執(zhí)行過程中發(fā)生異常,JavaScript
引擎會拋出異常并停止執(zhí)行代碼。
3.3、銷毀階段
執(zhí)行上下文銷毀階段是指當(dāng)一個函數(shù)執(zhí)行完畢或者當(dāng)前執(zhí)行上下文被彈出執(zhí)行上下文棧時,執(zhí)行上下文會被銷毀的過程。在執(zhí)行上下文銷毀階段,JavaScript引擎
會執(zhí)行以下步驟:
- 垃圾回收:
JavaScript
引擎會檢查當(dāng)前執(zhí)行上下文中的變量對象和函數(shù)聲明是否被其他對象引用。如果沒有被引用,則這些對象將被標(biāo)記為垃圾對象,并在垃圾回收過程中被清除。 - 變量銷毀:
JavaScript
引擎會銷毀當(dāng)前執(zhí)行上下文中的所有變量。在函數(shù)執(zhí)行結(jié)束時,所有局部變量將被銷毀。在全局執(zhí)行上下文中,全局變量只有在頁面關(guān)閉時才會被銷毀。 - 閉包變量銷毀:如果當(dāng)前執(zhí)行上下文是一個閉包函數(shù),那么其中的閉包變量將不會被銷毀。這是因為閉包變量被外層函數(shù)的作用域鏈所引用,只有當(dāng)外層函數(shù)被銷毀時,閉包變量才會被銷毀。
- 執(zhí)行上下文彈出:
JavaScript
引擎會將當(dāng)前執(zhí)行上下文從執(zhí)行上下文棧中彈出,并將控制權(quán)返回給上一個執(zhí)行上下文。
小知識:
ES5
以上的規(guī)范,對于執(zhí)行上下文的創(chuàng)建過程有所調(diào)整,移除了了ES3
中的變量對象VO
和活動對象AO
,引入了詞法環(huán)境組件(LexicalEnvironment component
) 和變量環(huán)境組件(VariableEnvironment component
)。
4、執(zhí)行棧
執(zhí)行棧又稱調(diào)用棧,記錄了腳本整個生命周期中生成的執(zhí)行上下文。
小知識:
JavaScrip
是單線程語言,也就是說它只能在同一時間執(zhí)行一項任務(wù)。因此,其他的操作、函數(shù)和事件發(fā)生時,執(zhí)行上下文也會被創(chuàng)建。由于單線程的特性,一個堆疊了執(zhí)行上下文的棧就會被創(chuàng)建,稱為執(zhí)行棧
。
JS引擎
會搜索代碼中被調(diào)用的函數(shù)。每一次函數(shù)被調(diào)用,一個新的FEC
就會被創(chuàng)建,并被放置在當(dāng)前執(zhí)行上下文的上方。而執(zhí)行棧最頂部的執(zhí)行上下文會成為活躍執(zhí)行上下文
,并且始終是JS引擎
優(yōu)先執(zhí)行。
一旦活躍執(zhí)行上下文中的代碼被執(zhí)行完畢,JS引擎就會從執(zhí)行棧中彈出這個執(zhí)行上下文,緊接著執(zhí)行下一個執(zhí)行上下文,以此類推。
4.1、示例代碼
用一段代碼來描述執(zhí)行棧的流程
var name = "Guizimo"; function first() { var a = "Hi!"; second(); console.log(`${a} ${name}`); } function second() { var b = "Hey!"; third(); console.log(`$ ${name}`); } function third() { var c = "Hello!"; console.log(`${c} ${name}`); } first();
執(zhí)行結(jié)果:
Hello! Guizimo
Hey! Guizimo
Hi! Guizimo
對于這個預(yù)料之中,但總感覺奇奇怪怪的結(jié)果...所以還是使用圖示來講解一下。
4.2、圖示講解
JS引擎
加載腳本,創(chuàng)建GEC
,并壓入執(zhí)行棧的最底部。name
變量,first
、second
和third
函數(shù)在所有函數(shù)外部定義,所以位于GEC
,并且被VO
存儲。
當(dāng)JS引擎
遇到first
函數(shù)調(diào)用時,一個新的FEC
被創(chuàng)建。新的執(zhí)行上下文被放置在當(dāng)前上下文上方,形成執(zhí)行棧
。在first
函數(shù)調(diào)用時,其執(zhí)行上下文變成活躍執(zhí)行上下文。在first
函數(shù)中的變量a ='Hi!'
被存儲在其FEC
中,而非GEC
中。
緊接著,second
函數(shù)在first
函數(shù)中被調(diào)用。由于JavaScript
單線程的特性,first
函數(shù)的執(zhí)行會被暫停,直到second
函數(shù)執(zhí)行完閉,才會繼續(xù)執(zhí)行。同樣的,JS引擎
會給second
函數(shù)設(shè)置一個新的FEC
,并把它放置在棧頂端,并激活。second
函數(shù)成為活躍執(zhí)行上下文,變量b = 'Hey!'
被存儲在其FEC
中。
再之后second
函數(shù)中的third
函數(shù)被調(diào)用,其FEC
被創(chuàng)建并放置在執(zhí)行棧的頂部。
在third
函數(shù)中的變量c = 'Hello!'
被存儲在其FEC中,Hello! Guizimo
在控制臺中打印。等待third
函數(shù)執(zhí)行完畢后, 其FEC
就從棧頂端彈出,而調(diào)用third
函數(shù)的second
函數(shù)重新成為活躍執(zhí)行上下文。
回到second
函數(shù),控制臺打印Hey! Guizimo
。函數(shù)執(zhí)行完成所有任務(wù),這個執(zhí)行上下文從執(zhí)行棧上彈出。
當(dāng)first
函數(shù)執(zhí)行完畢,從執(zhí)行棧上彈出后,控制流回到代碼的GEC
。
最終,所有代碼執(zhí)行完畢,JS引擎
把GEC
從執(zhí)行棧上彈出。
以上就是詳解JavaScript中的執(zhí)行上下文的詳細內(nèi)容,更多關(guān)于JavaScript執(zhí)行上下文的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ECharts柱狀排名圖柱子上方顯示文字與圖標(biāo)代碼實例
我們在繪制柱狀圖時如果想要柱條上顯示文字,可以參考本文,這篇文章主要給大家介紹了關(guān)于ECharts柱狀排名圖柱子上方顯示文字與圖標(biāo)的相關(guān)資料,需要的朋友可以參考下2023-11-11微信小程序?qū)崿F(xiàn)動態(tài)獲取元素寬高的方法分析
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)動態(tài)獲取元素寬高的方法,結(jié)合實例形式分析了微信小程序動態(tài)獲取、設(shè)置元素寬高的相關(guān)操作技巧與注意事項,需要的朋友可以參考下2018-12-1220分鐘成功編寫bootstrap響應(yīng)式頁面 就這么簡單
這篇文章主要教大家如何在20分鐘內(nèi)成功編寫bootstrap響應(yīng)式頁面,其實很簡單,培養(yǎng)大家分分鐘開發(fā)出一個高大上的頁面能力,感興趣的小伙伴們可以參考一下2016-05-05微信小程序使用GoEasy實現(xiàn)websocket實時通訊
這篇文章主要介紹了微信小程序使用GoEasy實現(xiàn)websocket實時通訊的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05