詳解JS中的堆棧,事件循環(huán),執(zhí)行上下文和作用域以及閉包
1. 堆棧
在JavaScript中,內(nèi)存堆是內(nèi)存分配的地方,調(diào)用棧是代碼執(zhí)行的地方。
原始類型的保存方式:在變量中保存的是值本身,所以原始類型也被稱之為值類型。
對象類型的保存方式:在變量中保存的是對象的“引用”,所以對象類型也被稱之為引用類型。
調(diào)用棧理解非常簡單,當(dāng)遇見一個方法時推入調(diào)用棧中,執(zhí)行一個方法彈出棧,每一個方法稱為一個調(diào)用幀。
2. 事件循環(huán)
理解了堆棧之后,接著來看一下與之相關(guān)的事件循環(huán)。
首先需要明確的是JavaScript是單線程語言,所有代碼都執(zhí)行在一個線程中,這通常會導(dǎo)致一個問題,當(dāng)一個方法耗時過長,整個頁面隨之卡住,所以為了避免這種情況發(fā)生,JavaScript中存在事件循環(huán)的機制(并非JavaScript創(chuàng)造),來循環(huán)執(zhí)行事件,堵塞的事件通過循環(huán)在后期再來判斷是否執(zhí)行完成,比如讀取接口,后期再來看接口是否請求完成,請求完成之后再執(zhí)行對應(yīng)的回調(diào)函數(shù)(接口請求是瀏覽器提供的能力,不占用單線程)。
事件循環(huán)也就是將任務(wù)分為同步任務(wù)和異步任務(wù),任務(wù)按照順序進行執(zhí)行。
事件循環(huán)中一個重要概念是宏任務(wù)和微任務(wù),宏任務(wù)也就是線程中首先一輪執(zhí)行的函數(shù),微任務(wù)也就是宏任務(wù)里面的任務(wù),類似進程和線程的關(guān)系,宏任務(wù)是進程,微任務(wù)是線程,下面來看一下三者之間的關(guān)系:
事件循環(huán),其實循環(huán)的就是宏任務(wù)和微任務(wù),當(dāng)宏任務(wù)中有微任務(wù)時,執(zhí)行里面的微任務(wù)。
下面來看一下在JavaScript中具體哪些函數(shù)是宏任務(wù),哪些是微任務(wù):
- macro-task(宏任務(wù)):包括整體代碼script,setTimeout,setInterval
- micro-task(微任務(wù)):Promise,process.nextTick(node代碼, 類似vue中this.$nextTick)
具體來看一下執(zhí)行流程:
- 整體script作為第一個宏任務(wù)進入主線程;
- 遇到
setTimeout
、setInterval
,其回調(diào)函數(shù)被分發(fā)到宏任務(wù)事件隊列中; - 遇到
process.nextTick()
,其回調(diào)函數(shù)被分發(fā)到微任務(wù)事件隊列中; - 遇到
Promise
,new Promise
函數(shù)體內(nèi)容直接執(zhí)行。then
等回調(diào)部分被分發(fā)到微任務(wù)事件隊列中; - 微任務(wù)在宏任務(wù)執(zhí)行后開始執(zhí)行,比如微任務(wù)屬于第一個宏任務(wù),那么第一個宏任務(wù)執(zhí)行完,就執(zhí)行第一個宏任務(wù)里面的微任務(wù),也就是說
script
里面要是包含微任務(wù),那么是先于setTimeout
等第二輪執(zhí)行的宏任務(wù)的; - 第一輪執(zhí)行完成后,開始第二輪,也就是
setTimeout
、setInterval
回調(diào)函數(shù)里面的內(nèi)容,屬于第二輪宏任務(wù),如果里面包含微任務(wù),那么緊接著回調(diào)函數(shù)里面內(nèi)容執(zhí)行完之后開始執(zhí)行; - 如果微任務(wù)里面還包含微任務(wù),那么是緊接著外層的微任務(wù)開始執(zhí)行的。
注意在node有一些不同,存在下面的優(yōu)先級順序:process.nextTick() > Promise.then() > setTimeout > setImmediate
下面來看一個具體的例子:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
第一輪事件循環(huán)流程分析如下:
- 整體script作為第一個宏任務(wù)進入主線程,遇到
console.log
,輸出1。 - 遇到
setTimeout
,其回調(diào)函數(shù)被分發(fā)到宏任務(wù)Event Queue中。我們暫且記為setTimeout1
。 - 遇到
process.nextTick()
,其回調(diào)函數(shù)被分發(fā)到微任務(wù)Event Queue中。我們記為process1
。 - 遇到
Promise
,new Promise
直接執(zhí)行,輸出7。then
被分發(fā)到微任務(wù)Event Queue中。我們記為then1
。 - 又遇到了
setTimeout
,其回調(diào)函數(shù)被分發(fā)到宏任務(wù)Event Queue中,我們記為setTimeout2
。
宏任務(wù)Event Queue | 微任務(wù)Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
- 上表是第一輪事件循環(huán)宏任務(wù)結(jié)束時各Event Queue的情況,此時已經(jīng)輸出了1和7。
- 我們發(fā)現(xiàn)了
process1
和then1
兩個微任務(wù)。 - 執(zhí)行
process1
,輸出6。 - 執(zhí)行
then1
,輸出8。
好了,第一輪事件循環(huán)正式結(jié)束,這一輪的結(jié)果是輸出1,7,6,8。那么第二輪時間循環(huán)從setTimeout1
宏任務(wù)開始:
首先輸出2。接下來遇到了process.nextTick()
,同樣將其分發(fā)到微任務(wù)Event Queue中,記為process2
。new Promise
立即執(zhí)行輸出4,then
也分發(fā)到微任務(wù)Event Queue中,記為then2
。
宏任務(wù)Event Queue | 微任務(wù)Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
- 第二輪事件循環(huán)宏任務(wù)結(jié)束,我們發(fā)現(xiàn)有
process2
和then2
兩個微任務(wù)可以執(zhí)行。 - 輸出3。
- 輸出5。
- 第二輪事件循環(huán)結(jié)束,第二輪輸出2,4,3,5。
- 第三輪事件循環(huán)開始,此時只剩setTimeout2了,執(zhí)行。
- 直接輸出9。
- 將
process.nextTick()
分發(fā)到微任務(wù)Event Queue中。記為process3
。 - 直接執(zhí)行
new Promise
,輸出11。 - 將
then
分發(fā)到微任務(wù)Event Queue中,記為then3
。
宏任務(wù)Event Queue | 微任務(wù)Event Queue |
---|---|
process3 | |
then3 |
- 第三輪事件循環(huán)宏任務(wù)執(zhí)行結(jié)束,執(zhí)行兩個微任務(wù)
process3
和then3
。 - 輸出10。
- 輸出12。
- 第三輪事件循環(huán)結(jié)束,第三輪輸出
9,11,10,12
。
整段代碼,共進行了三次事件循環(huán),完整的輸出為1,7,6,8,2,4,3,5,9,11,10,12
。 (請注意,node環(huán)境下的事件監(jiān)聽依賴libuv與前端環(huán)境不完全相同,輸出順序可能會有誤差)。
3. 執(zhí)行上下文
接著來看一下執(zhí)行上下文,簡而言之,執(zhí)行上下文是評估和執(zhí)行 JavaScript 代碼的環(huán)境的抽象概念。每當(dāng) Javascript 代碼在運行的時候,它都是在執(zhí)行上下文中運行。
JavaScript 中有三種執(zhí)行上下文類型:
- 全局執(zhí)行上下文 — 這是默認或者說基礎(chǔ)的上下文,任何不在函數(shù)內(nèi)部的代碼都在全局上下文中。它會執(zhí)行兩件事:創(chuàng)建一個全局的 window 對象(瀏覽器的情況下),并且設(shè)置
this
的值等于這個全局對象。一個程序中只會有一個全局執(zhí)行上下文。 - 函數(shù)執(zhí)行上下文 — 每當(dāng)一個函數(shù)被調(diào)用時, 都會為該函數(shù)創(chuàng)建一個新的上下文。每個函數(shù)都有它自己的執(zhí)行上下文,不過是在函數(shù)被調(diào)用時創(chuàng)建的。函數(shù)上下文可以有任意多個。每當(dāng)一個新的執(zhí)行上下文被創(chuàng)建,它會按定義的順序(將在后文討論)執(zhí)行一系列步驟。
- Eval 函數(shù)執(zhí)行上下文 — 執(zhí)行在
eval
函數(shù)內(nèi)部的代碼也會有它屬于自己的執(zhí)行上下文,但由于 JavaScript 開發(fā)者并不經(jīng)常使用eval
,所以在這里不會討論。
總結(jié)一下,執(zhí)行上下文大體分為全局和函數(shù)執(zhí)行上下文,也就是執(zhí)行環(huán)境,函數(shù)可以讀取外部函數(shù)的變量,通常也稱為閉包,通過這個原理,相比靜態(tài)語言,可以更靈活的獲取外部的參數(shù)。
執(zhí)行上下文的不同,直接導(dǎo)致 this
值內(nèi)容的不同。
同時一個執(zhí)行上下文將會創(chuàng)建一個上面的執(zhí)行棧,而不是所有的執(zhí)行上下文的所有方法共用一個執(zhí)行棧。
4. 作用域
作用域這個內(nèi)容非常簡單,基本上所有語言都存在作用域,在JavaScript中,需要注意一點,函數(shù)中創(chuàng)建的值是在創(chuàng)建的時候獲得的,而不是調(diào)用,通過代碼來看一下:
let x = 10 function fn() { x = 20 console.log(x) } function foo() { x = 30 fn() // 20 } foo()
上面代碼打印的值仍然是20,因為創(chuàng)建 fn
函數(shù)時,對應(yīng)的作用域里面的值為20,而不是調(diào)用 fn
時,foo函數(shù)作用域里面的值。
這里有一個注意點,我們來看下面的代碼:
let x = 10 function fn() { console.log(x) } function foo() { x = 30 fn() // 30 } foo()
上面的代碼會打印30,這是怎么回事,不是說再創(chuàng)建的位置取值嗎?
答案是,確實是在創(chuàng)建的位置,但是先執(zhí)行的foo函數(shù),把外層的x的值變更了,下面的代碼能解釋這個問題:
let x = 10 function fn() { console.log(x) } function foo() { let x = 30 fn() // 10 } foo()
可以看到,打印的其實并不是foo函數(shù)里的值,而是創(chuàng)建函數(shù)時的值。
接著我們要理一下,什么是創(chuàng)建時的值,這里要引出一個概念,作用域鏈,也就是取值的鏈條:
- 現(xiàn)在當(dāng)前作用域查找a,如果有則獲取并結(jié)束,如果沒有則繼續(xù);
- 如果當(dāng)前作用域是全局作用域,則證明a未定義,結(jié)束,否則繼續(xù);
- (不是全局作用域,那就是函數(shù)作用域)將創(chuàng)建該函數(shù)的作用域作為當(dāng)前作用域;
- 跳轉(zhuǎn)到第一步。
var a = 10 function fn() { var b = 20 function bar() { console.log(a) // 10 console.log(b) // 20 } return bar } var x = fn() var b = 200 x()
總結(jié)一下
函數(shù)上下文環(huán)境是在函數(shù)執(zhí)行時創(chuàng)建的,同時在上下文中生成了對應(yīng)的變量,同一個函數(shù)根據(jù)傳遞進來的參數(shù)不同,里面的變量也會不同;
而作用域是函數(shù)創(chuàng)建時就產(chǎn)生了,作用域作用域,說白了就是這個函數(shù)自己的地盤,無論是否調(diào)用,反正這個函數(shù)都擁有這個地盤了;
只有當(dāng)調(diào)用時才會創(chuàng)建上下文環(huán)境,并且可能不止一個,比如通過傳遞不同參數(shù),可能會創(chuàng)建多個上下文環(huán)境,上下文環(huán)境說白了就是在這個環(huán)境中變量的值是什么,以便使用。
5. 閉包
前面鋪墊了那么多內(nèi)容,主要是用于引出閉包,閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。 在javascript中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,所以閉包可以理解成“定義在一個函數(shù)內(nèi)部的函數(shù)“。 在本質(zhì)上,閉包是將函數(shù)內(nèi)部和函數(shù)外部連接起來的橋梁。
下面我們來看看閉包運用的兩種形式:
第一,函數(shù)作為返回值:
function fn() { var max = 100 return function bar(x) { if (x > max) { console.log(x) } } } var f1 = fn() f1(115)
上面返回的內(nèi)部函數(shù)就是一個閉包,它可以讀取其外部fn函數(shù)的max值,從這種情況來說,下面的情況也是閉包:
var max = 100 function fn() { console.log(max) } fn()
從上面兩段代碼可以看出,所有的函數(shù)其實只要函數(shù)內(nèi)部能夠讀取了其外部的變量,都可以稱為閉包,也就是說,所有函數(shù)都是閉包,因為一個函數(shù)最少也是可以讀取全局環(huán)境下的變量的,只是第二段代碼通常不是閉包的常見使用形式,常見的使用形式還是將函數(shù)作為返回值
第二,函數(shù)作為參數(shù)傳遞:
var max = 10 var fn = function (x) { if (x > 100) { console.log(x) // 不打印任何東西 } } ;(function (f) { var max = 100 f(15) })(fn)
函數(shù)作為參數(shù)傳遞,進入另一個函數(shù)作為另一個函數(shù)的內(nèi)容,此時傳遞的這個函數(shù)就是一個閉包,注意一下,這里的max根據(jù)前面的作用域原則,是讀取函數(shù)定義時的max,而不是調(diào)用時。
以上就是詳解JS中的堆棧,事件循環(huán),執(zhí)行上下文和作用域以及閉包的詳細內(nèi)容,更多關(guān)于JS事件循環(huán) 執(zhí)行上下文 作用域 閉包的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決 viewer.js 動態(tài)更新圖片導(dǎo)致無法預(yù)覽的問題
Viewer.js 是一款強大的圖片查看器,這篇文章主要介紹了解決 viewer.js 動態(tài)更新圖片導(dǎo)致無法預(yù)覽的問題 ,需要的朋友可以參考下2019-05-05JS中的算法與數(shù)據(jù)結(jié)構(gòu)之隊列(Queue)實例詳解
這篇文章主要介紹了JS中的算法與數(shù)據(jù)結(jié)構(gòu)之隊列(Queue),結(jié)合實例形式詳細分析了javascript中隊列的概念、原理、定義及常見操作技巧,需要的朋友可以參考下2019-08-08echarts柱狀圖背景重疊組合而非并列的實現(xiàn)代碼
這篇文章主要給大家介紹了關(guān)于echarts柱狀圖背景重疊組合而非并列的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12