<acronym id="indot"><dfn id="indot"></dfn></acronym>
<span id="indot"></span>

<bdo id="indot"><meter id="indot"></meter></bdo>
<label id="indot"><samp id="indot"></samp></label>
<label id="indot"><xmp id="indot">
  • <span id="indot"><table id="indot"></table></span>
    <center id="indot"><optgroup id="indot"></optgroup></center>
  • <bdo id="indot"><meter id="indot"></meter></bdo>
      當前位置:首頁 > javascript > 正文內(nèi)容

      深入剖析JavaScript中的堆棧、事件循環(huán)、執(zhí)行上下文、作用域和閉包

      hxing6411年前 (2024-01-13)javascript4188
      Document

      1. 堆棧

      在JavaScript中,內(nèi)存堆是內(nèi)存分配的地方,調(diào)用棧是代碼執(zhí)行的地方。

      原始類型的保存方式:在變量中保存的是值本身,所以原始類型也被稱之為值類型。

      對象類型的保存方式:在變量中保存的是對象的“引用”,所以對象類型也被稱之為引用類型。

      調(diào)用棧理解非常簡單,當遇見一個方法時推入調(diào)用棧中,執(zhí)行一個方法彈出棧,每一個方法稱為一個調(diào)用幀。


      2. 事件循環(huán)

      理解了堆棧之后,接著來看一下與之相關(guān)的事件循環(huán)。

      首先需要明確的是JavaScript是單線程語言,所有代碼都執(zhí)行在一個線程中,這通常會導(dǎo)致一個問題,當一個方法耗時過長,整個頁面隨之卡住,所以為了避免這種情況發(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ù),當宏任務(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í)行流程:

      1. 整體script作為第一個宏任務(wù)進入主線程;

      2. 遇到setTimeout 、 setInterval,其回調(diào)函數(shù)被分發(fā)到宏任務(wù)事件隊列中;

      3. 遇到process.nextTick(),其回調(diào)函數(shù)被分發(fā)到微任務(wù)事件隊列中;

      4. 遇到Promise,new Promise函數(shù)體內(nèi)容直接執(zhí)行。then等回調(diào)部分被分發(fā)到微任務(wù)事件隊列中;

      5. 微任務(wù)在宏任務(wù)執(zhí)行后開始執(zhí)行,比如微任務(wù)屬于第一個宏任務(wù),那么第一個宏任務(wù)執(zhí)行完,就執(zhí)行第一個宏任務(wù)里面的微任務(wù),也就是說 script 里面要是包含微任務(wù),那么是先于 setTimeout 等第二輪執(zhí)行的宏任務(wù)的;

      6. 第一輪執(zhí)行完成后,開始第二輪,也就是setTimeout 、 setInterval 回調(diào)函數(shù)里面的內(nèi)容,屬于第二輪宏任務(wù),如果里面包含微任務(wù),那么緊接著回調(diào)函數(shù)里面內(nèi)容執(zhí)行完之后開始執(zhí)行;

      7. 如果微任務(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)了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
      setTimeout2process2

      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)境的抽象概念。每當 Javascript 代碼在運行的時候,它都是在執(zhí)行上下文中運行。


      JavaScript 中有三種執(zhí)行上下文類型:

      • 全局執(zhí)行上下文 — 這是默認或者說基礎(chǔ)的上下文,任何不在函數(shù)內(nèi)部的代碼都在全局上下文中。它會執(zhí)行兩件事:創(chuàng)建一個全局的 window 對象(瀏覽器的情況下),并且設(shè)置 this 的值等于這個全局對象。一個程序中只會有一個全局執(zhí)行上下文。

      • 函數(shù)執(zhí)行上下文 — 每當一個函數(shù)被調(diào)用時, 都會為該函數(shù)創(chuàng)建一個新的上下文。每個函數(shù)都有它自己的執(zhí)行上下文,不過是在函數(shù)被調(diào)用時創(chuàng)建的。函數(shù)上下文可以有任意多個。每當一個新的執(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)在當前作用域查找a,如果有則獲取并結(jié)束,如果沒有則繼續(xù);

      • 如果當前作用域是全局作用域,則證明a未定義,結(jié)束,否則繼續(xù);

      • (不是全局作用域,那就是函數(shù)作用域)將創(chuàng)建該函數(shù)的作用域作為當前作用域;

      • 跳轉(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ù)都擁有這個地盤了;

      • 只有當調(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ù)外部連接起來的橋梁。


      下面我們來看看閉包運用的兩種形式:

      1. 函數(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ù)作為返回值


      2. 函數(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)用時。



      掃描二維碼推送至手機訪問。

      版權(quán)聲明:本文由星星博客發(fā)布,如需轉(zhuǎn)載請注明出處。

      本文鏈接:http://www.7811333.com/?id=511

      “深入剖析JavaScript中的堆棧、事件循環(huán)、執(zhí)行上下文、作用域和閉包” 的相關(guān)文章

      js模塊化歷程

      這是一篇關(guān)于js模塊化歷程的長長的流水賬,記錄js模塊化思想的誕生與變遷,展望ES6模塊化標準的未來。經(jīng)歷過這段歷史的人或許會感到滄桑,沒經(jīng)歷過的人也應(yīng)該知道這段歷史。 無模塊時代在ajax還未提出之前,js還只是一種“玩具語言”,由Brendan Eich花了不到十天時間發(fā)明,用來在網(wǎng)頁...

      深入理解JavaScript原型鏈

      JavaScript原型鏈是該語言中一個核心的概念,理解它對于成為一名優(yōu)秀的JavaScript開發(fā)者至關(guān)重要。在本篇文章中,我們將深入探討JavaScript原型鏈的工作原理、如何使用原型鏈來實現(xiàn)繼承,以及一些與原型相關(guān)的最佳實踐。 原型鏈是什么?...

      JavaScript中的call、bind和apply方法深度解析

      JavaScript是一種動態(tài)的、強大的編程語言,它的靈活性源于其獨特的函數(shù)調(diào)用方式。在JavaScript中,我們常常會遇到三個非常重要的函數(shù)方法:call、bind和apply。這些方法都是用來改變函數(shù)運行時this的指向的。理解它們的工作原理和使用場景,對于我們編寫高質(zhì)量的Java...

      JS數(shù)組常用方法

      JS數(shù)組常用方法

      JS數(shù)組常用方法:1.數(shù)組的復(fù)制和填充批量復(fù)制方法 copyWithin(),以及填充數(shù)組方法fill()。這兩個方法的函數(shù)簽名類似,都需要指定既有數(shù)組實例上的一個范圍,包含開始索引,不包含結(jié)束索引。使用這個方法不會改變數(shù)組的大小。 1.1.fill()方法使用fill()方法可以向一個已...

      微信小程序如何下載外部文件

      小程序下載文件主要是通過轉(zhuǎn)發(fā)文件到聊天進行轉(zhuǎn)存,以下為具體實現(xiàn)代碼,可以直接拿去用,傳入下載的url:const download = (url) => {   var that = this...

      typescript 中 omit 的理解

      在 TypeScript 中,Omit 是一種非常有用的工具類型,它可以用于創(chuàng)建一個新的類型,這個新類型是從現(xiàn)有類型中排除了指定的屬性后得到的。如果你想要處理一個對象但又不需要包含某些屬性,那么 Omit 可以幫助你快速地創(chuàng)建一個新的類型。 比如說,...

      發(fā)表評論

      訪客

      ◎歡迎參與討論,請在這里發(fā)表您的看法和觀點。
      主站蜘蛛池模板: 伊人久久大香线蕉综合5g| 中文字幕久久综合| 91精品国产综合久久久久| 少妇熟女久久综合网色欲| 久久一日本道色综合久久m| 色婷五月综激情亚洲综合| 天天综合久久一二三区| 亚洲综合国产精品| 一本久道久久综合狠狠躁AV| 色综合久久无码中文字幕| 色综合久久久无码中文字幕| 国产一级a爱做综合| 麻豆久久婷婷综合五月国产| 伊人久久大香线蕉综合爱婷婷| 天天干天天色综合| 亚洲人成依人成综合网| 久久婷婷五月综合成人D啪| 亚洲综合激情五月丁香六月| 久久天堂AV综合合色蜜桃网| 99久久国产综合精品麻豆| 色综合伊人色综合网站| 国产香蕉尹人综合在线观看| 色悠久久久久综合网香蕉| 色欲综合一区二区三区| 在线亚洲97se亚洲综合在线 | 国产成人综合精品| 国产精品亚洲综合五月天| 91亚洲精品第一综合不卡播放| 综合国产精品第一页| 久久久久久青草大香综合精品| heyzo专区无码综合| 亚洲欧洲国产成人综合在线观看 | 色狠台湾色综合网站| 一本色道久久综合无码人妻 | 狠狠色丁香久久婷婷综合| 狠狠色综合网站久久久久久久高清 | 精品亚洲综合在线第一区| 亚洲人成伊人成综合网久久久| 狠狠色狠狠色综合曰曰| 亚洲综合激情九月婷婷| 色青青草原桃花久久综合|