亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

詳解JS中的堆棧,事件循環(huán),執(zhí)行上下文和作用域以及閉包

 更新時間:2024年01月11日 08:20:03   作者:easylee  
這篇文章主要為大家詳細介紹了JavaScript中的堆棧,事件循環(huán),執(zhí)行上下文和作用域以及閉包的相關(guān)知識,文中的示例代碼講解詳細,感興趣的小伙伴可以了解下

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ù)事件隊列中;
  • 遇到Promisenew 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
setTimeout1process1
setTimeout2then1
  • 上表是第一輪事件循環(huán)宏任務(wù)結(jié)束時各Event Queue的情況,此時已經(jīng)輸出了1和7。
  • 我們發(fā)現(xiàn)了process1then1兩個微任務(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
setTimeout2process2
then2
  • 第二輪事件循環(huán)宏任務(wù)結(jié)束,我們發(fā)現(xiàn)有process2then2兩個微任務(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ù)process3then3
  • 輸出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)文章

最新評論