成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

搞懂JavaScript引擎運(yùn)行原理

lastSeries / 2231人閱讀

摘要:同步一次執(zhí)行一件事,同步引擎一次只執(zhí)行一行,是同步的。調(diào)用函數(shù)將其推入堆棧并從函數(shù)返回將其彈出堆棧。執(zhí)行上下文當(dāng)函數(shù)放入到調(diào)用堆棧時由創(chuàng)建的環(huán)境。執(zhí)行結(jié)果它會立即被推到回調(diào)隊列,但它仍然會等待調(diào)用堆棧為空才會執(zhí)行。

為了保證可讀性,本文采用意譯而非直譯。

想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你!

一些名詞

JS引擎 — 一個讀取代碼并運(yùn)行的引擎,沒有單一的“JS引擎”;,每個瀏覽器都有自己的引擎,如谷歌有V。

作用域 — 可以從中訪問變量的“區(qū)域”。

詞法作用域— 在詞法階段的作用域,換句話說,詞法作用域是由你在寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定的,因此當(dāng)詞法分析器處理代碼時會保持作用域不變。

塊作用域 — 由花括號{}創(chuàng)建的范圍

作用域鏈 — 函數(shù)可以上升到它的外部環(huán)境(詞法上)來搜索一個變量,它可以一直向上查找,直到它到達(dá)全局作用域。

同步 — 一次執(zhí)行一件事, “同步”引擎一次只執(zhí)行一行,JavaScript是同步的。

異步 — 同時做多個事,JS通過瀏覽器API模擬異步行為

事件循環(huán)(Event Loop) - 瀏覽器API完成函數(shù)調(diào)用的過程,將回調(diào)函數(shù)推送到回調(diào)隊列(callback queue),然后當(dāng)堆棧為空時,它將回調(diào)函數(shù)推送到調(diào)用堆棧。

堆棧 —一種數(shù)據(jù)結(jié)構(gòu),只能將元素推入并彈出頂部元素。 想想堆疊一個字形的塔樓; 你不能刪除中間塊,后進(jìn)先出。

— 變量存儲在內(nèi)存中。

調(diào)用堆棧 — 函數(shù)調(diào)用的隊列,它實現(xiàn)了堆棧數(shù)據(jù)類型,這意味著一次可以運(yùn)行一個函數(shù)。 調(diào)用函數(shù)將其推入堆棧并從函數(shù)返回將其彈出堆棧。

執(zhí)行上下文 — 當(dāng)函數(shù)放入到調(diào)用堆棧時由JS創(chuàng)建的環(huán)境。

閉包 — 當(dāng)在另一個函數(shù)內(nèi)創(chuàng)建一個函數(shù)時,它“記住”它在以后調(diào)用時創(chuàng)建的環(huán)境。

垃圾收集 — 當(dāng)內(nèi)存中的變量被自動刪除時,因為它不再使用,引擎要處理掉它。

變量的提升— 當(dāng)變量內(nèi)存沒有賦值時會被提升到全局的頂部并設(shè)置為undefined。

this —由JavaScript為每個新的執(zhí)行上下文自動創(chuàng)建的變量/關(guān)鍵字。

調(diào)用堆棧(Call Stack)

看看下面的代碼:

var myOtherVar = 10

function a() {
  console.log("myVar", myVar)
  b()
}

function b() {
  console.log("myOtherVar", myOtherVar)
  c()
}

function c() {
  console.log("Hello world!")
}

a()

var myVar = 5

有幾個點需要注意:

變量聲明的位置(一個在上,一個在下)

函數(shù)a調(diào)用下面定義的函數(shù)b, 函數(shù)b調(diào)用函數(shù)c

當(dāng)它被執(zhí)行時你期望發(fā)生什么? 是否發(fā)生錯誤,因為ba之后聲明或者一切正常? console.log 打印的變量又是怎么樣?

以下是打印結(jié)果:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

來分解一下上述的執(zhí)行步驟。

1. 變量和函數(shù)聲明(創(chuàng)建階段)

第一步是在內(nèi)存中為所有變量和函數(shù)分配空間。 但請注意,除了undefined之外,尚未為變量分配值。 因此,myVar在被打印時的值是undefined,因為JS引擎從頂部開始逐行執(zhí)行代碼。

函數(shù)與變量不一樣,函數(shù)可以一次聲明和初始化,這意味著它們可以在任何地方被調(diào)用。

所以以上代碼看起來像這樣子:

var myOtherVar = undefined
var myVar = undefined

function a() {...}
function b() {...}
function c() {...}

這些都存在于JS創(chuàng)建的全局上下文中,因為它位于全局空間中。

在全局上下文中,JS還添加了:

全局對象(瀏覽器中是 window 對象,NodeJs 中是 global 對象)

this 指向全局對象

2. 執(zhí)行

接下來,JS 引擎會逐行執(zhí)行代碼。

myOtherVar = 10在全局上下文中,myOtherVar被賦值為10

已經(jīng)創(chuàng)建了所有函數(shù),下一步是執(zhí)行函數(shù) a()

每次調(diào)用函數(shù)時,都會為該函數(shù)創(chuàng)建一個新的上下文(重復(fù)步驟1),并將其放入調(diào)用堆棧。

function a() {
  console.log("myVar", myVar)
  b()
}

如下步驟:

創(chuàng)建新的函數(shù)上下文

a 函數(shù)里面沒有聲明變量和函數(shù)

函數(shù)內(nèi)部創(chuàng)建了 this 并指向全局對象(window)

接著引用了外部變量 myVarmyVar 屬于全局作用域的。

接著調(diào)用函數(shù) b ,函數(shù)b的過程跟 a一樣,這里不做分析。

下面調(diào)用堆棧的執(zhí)行示意圖:

創(chuàng)建全局上下文,全局變量和函數(shù)。

每個函數(shù)的調(diào)用,會創(chuàng)建一個上下文,外部環(huán)境的引用及 this。

函數(shù)執(zhí)行結(jié)束后會從堆棧中彈出,并且它的執(zhí)行上下文被垃圾收集回收(閉包除外)。

當(dāng)調(diào)用堆棧為空時,它將從事件隊列中獲取事件。

作用域及作用域鏈

在前面的示例中,所有內(nèi)容都是全局作用域的,這意味著我們可以從代碼中的任何位置訪問它。 現(xiàn)在,介紹下私有作用域以及如何定義作用域。

函數(shù)/詞法作用域

考慮如下代碼:

function a() {
  var myOtherVar = "inside A"

  b()
}

function b() {
  var myVar = "inside B"

  console.log("myOtherVar:", myOtherVar)

  function c() {
    console.log("myVar:", myVar)
  }

  c()
}

var myOtherVar = "global otherVar"
var myVar = "global myVar"
a()

需要注意以下幾點:

全局作用域和函數(shù)內(nèi)部都聲明了變量

函數(shù)c現(xiàn)在在函數(shù)b中聲明

打印結(jié)果如下:

myOtherVar: "global otherVar"
myVar: "inside B"

執(zhí)行步驟:

全局創(chuàng)建和聲明 - 創(chuàng)建內(nèi)存中的所有函數(shù)和變量以及全局對象和 this

執(zhí)行 - 它逐行讀取代碼,給變量賦值,并執(zhí)行函數(shù)a

函數(shù)a創(chuàng)建一個新的上下文并被放入堆棧,在上下文中創(chuàng)建變量myOtherVar,然后調(diào)用函數(shù)b

函數(shù)b 也會創(chuàng)建一個新的上下文,同樣也被放入堆棧中

5,函數(shù)b的上下文中創(chuàng)建了 myVar 變量,并聲明函數(shù)c

上面提到每個新上下文會創(chuàng)建的外部引用,外部引用取決于函數(shù)在代碼中聲明的位置。

函數(shù)b試圖打印myOtherVar,但這個變量并不存在于函數(shù)b中,函數(shù)b 就會使用它的外部引用上作用域鏈向上找。由于函數(shù)b是全局聲明的,而不是在函數(shù)a內(nèi)部聲明的,所以它使用全局變量myOtherVar。

函數(shù)c執(zhí)行步驟一樣。由于函數(shù)c本身沒有變量myVar,所以它它通過作用域鏈向上找,也就是函數(shù)b,因為myVar函數(shù)b內(nèi)部聲明過。

下面是執(zhí)行示意圖:

請記住,外部引用是單向的,它不是雙向關(guān)系。例如,函數(shù)b不能直接跳到函數(shù)c的上下文中并從那里獲取變量。

最好將它看作一個只能在一個方向上運(yùn)行的鏈(范圍鏈)。

a -> global

c -> b -> global

在上面的圖中,你可能注意到,函數(shù)是創(chuàng)建新作用域的一種方式。(除了全局作用域)然而,還有另一種方法可以創(chuàng)建新的作用域,就是塊作用域。

塊作用域

下面代碼中,我們有兩個變量和兩個循環(huán),在循環(huán)重新聲明相同的變量,會打印什么(反正我是做錯了)?

function loopScope () {
  var i = 50
  var j = 99

  for (var i = 0; i < 10; i++) {}

  console.log("i =", i)

  for (let j = 0; j < 10; j++) {}

  console.log("j =", j)
}

loopScope()

打印結(jié)果:

i = 10
j = 99

第一個循環(huán)覆蓋了var i,對于不知情的開發(fā)人員來說,這可能會導(dǎo)致bug。

第二個循環(huán),每次迭代創(chuàng)建了自己作用域和變量。 這是因為它使用let關(guān)鍵字,它與var相同,只是let有自己的塊作用域。 另一個關(guān)鍵字是const,它與let相同,但const常量且無法更改(指內(nèi)存地址)。

塊作用域由大括號 {} 創(chuàng)建的作用域

再看一個例子:

function blockScope () {
  let a = 5
  {
    const blockedVar = "blocked"
    var b = 11

    a = 9000
  }

  console.log("a =", a)
  console.log("b =", b)
  console.log("blockedVar =", blockedVar)
}

blockScope()

打印結(jié)果:

a = 9000
b = 11
ReferenceError: blockedVar is not defined

a是塊作用域,但它在函數(shù)中,而不是嵌套的,本例中使用var是一樣的。

對于塊作用域的變量,它的行為類似于函數(shù),注意var b可以在外部訪問,但是const blockedVar不能。

在塊內(nèi)部,從作用域鏈向上找到 a 并將let a更改為9000。

使用塊作用域可以使代碼更清晰,更安全,應(yīng)該盡可能地使用它。

事件循環(huán)(Event Loop)

接下來看看事件循環(huán)。 這是回調(diào),事件和瀏覽器API工作的地方

我們沒有過多討論的事情是,也叫全局內(nèi)存。它是變量存儲的地方。由于了解JS引擎是如何實現(xiàn)其數(shù)據(jù)存儲的實際用途并不多,所以我們不在這里討論它。

來個異步代碼:

function logMessage2 () {
    console.log("Message 2")
}

console.log("Message 1")

setTimeout(logMessage2, 1000)

console.log("Message 3")

上述代碼主要是將一些 message 打印到控制臺。 利用setTimeout函數(shù)來延遲一條消息。 我們知道js是同步,來看看輸出結(jié)果

Message 1
Message 3
Message 2

打印 Message 1

調(diào)用 setTimeout

打印 Message 3

打印 Message 2

它記錄消息3

稍后,它會記錄消息2

setTimeout是一個 API,和大多數(shù)瀏覽器 API一樣,當(dāng)它被調(diào)用時,它會向瀏覽器發(fā)送一些數(shù)據(jù)和回調(diào)。我們這邊是延遲一秒打印 Message 2。

調(diào)用完setTimeout 后,我們的代碼繼續(xù)運(yùn)行,沒有暫停,打印 Message 3 并執(zhí)行一些必須先執(zhí)行的操作。

瀏覽器等待一秒鐘,它就會將數(shù)據(jù)傳遞給我們的回調(diào)函數(shù)并將其添加到事件/回調(diào)隊列中( event/callback queue)。 然后停留在隊列中,只有當(dāng)調(diào)用堆棧(call stack)為空時才會被壓入堆棧。

代碼示例

要熟悉JS引擎,最好的方法就是使用它,再來些有意義的例子。

簡單的閉包

這個例子中 有一個返回函數(shù)的函數(shù),并在返回的函數(shù)中使用外部的變量, 這稱為閉包。

function exponent (x) {
  return function (y) {
   //和math.pow() 或者x的y次方是一樣的
    return y ** x
  }
}

const square = exponent(2)

console.log(square(2), square(3)) // 4, 9

console.log(exponent(3)(2)) // 8
塊代碼

我們使用無限循環(huán)將將調(diào)用堆棧塞滿,會發(fā)生什么,回調(diào)隊列被會阻塞,因為只能在調(diào)用堆棧為空時添加回調(diào)隊列。

function blockingCode() {
  const startTime = new Date().getSeconds()

  // 延遲函數(shù)250毫秒
  setTimeout(function() {
    const calledAt = new Date().getSeconds()
    const diff = calledAt - startTime
 
    // 打印調(diào)用此函數(shù)所需的時間
    console.log(`Callback called after: ${diff} seconds`)
  }, 250)

  // 用循環(huán)阻塞堆棧2秒鐘
  while(true) {
    const currentTime = new Date().getSeconds()

    // 2 秒后退出
    if(currentTime - startTime >= 2) break
  }

}

blockingCode() // "Callback called after: 2 seconds"

我們試圖在250毫秒之后調(diào)用一個函數(shù),但因為我們的循環(huán)阻塞了堆棧所花了兩秒鐘,所以回調(diào)函數(shù)實際是兩秒后才會執(zhí)行,這是JavaScript應(yīng)用程序中的常見錯誤。

setTimeout不能保證在設(shè)置的時間之后調(diào)用函數(shù)。相反,更好的描述是,在至少經(jīng)過這段時間之后調(diào)用這個函數(shù)。

延遲函數(shù)

當(dāng) setTimeout 的設(shè)置為0,情況是怎么樣?

function defer () {
  setTimeout(() => console.log("timeout with 0 delay!"), 0)
  console.log("after timeout")
  console.log("last log")
}

defer()

你可能期望它被立即調(diào)用,但是,事實并非如此。

執(zhí)行結(jié)果:

after timeout
last log
timeout with 0 delay!

它會立即被推到回調(diào)隊列,但它仍然會等待調(diào)用堆棧為空才會執(zhí)行。

用閉包來緩存

Memoization是緩存函數(shù)調(diào)用結(jié)果的過程。

例如,有一個添加兩個數(shù)字的函數(shù)add。調(diào)用add(1,2)返回3,當(dāng)再次使用相同的參數(shù)add(1,2)調(diào)用它,這次不是重新計算,而是記住1 + 2是3的結(jié)果并直接返回對應(yīng)的結(jié)果。 Memoization可以提高代碼運(yùn)行速度,是一個很好的工具。

我們可以使用閉包實現(xiàn)一個簡單的memoize函數(shù)。

// 緩存函數(shù),接收一個函數(shù)
const memoize = (func) => {
  // 緩存對象
  // keys 是 arguments, values are results
  const cache = {}

  // 返回一個新的函數(shù)
  // it remembers the cache object & func (closure)
  // ...args is any number of arguments
  return (...args) => {
    // 將參數(shù)轉(zhuǎn)換為字符串,以便我們可以存儲它
    const argStr = JSON.stringify(args)

    // 如果已經(jīng)存,則打印
    console.log("cache", cache, !!cache[argStr])

    cache[argStr] = cache[argStr] || func(...args)

    return cache[argStr]
  }
}

const add = memoize((a, b) => a + b)

console.log("first add call: ", add(1, 2))

console.log("second add call", add(1, 2))

執(zhí)行結(jié)果:

cache {} false
first add call:  3
cache { "[1,2]": 3 } true
second add call 3

第一次 add 方法,緩存對象是空的,它調(diào)用我們的傳入函數(shù)來獲取值3.然后它將args/value鍵值對存儲在緩存對象中。

在第二次調(diào)用中,緩存中已經(jīng)有了,查找到并返回值。

對于add函數(shù)來說,有無緩存看起來無關(guān)緊要,甚至效率更低,但是對于一些復(fù)雜的計算,它可以節(jié)省很多時間。這個示例并不是一個完美的緩存示例,而是閉包的實際應(yīng)用。

代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進(jìn)行l(wèi)og 調(diào)試,這邊順便給大家推薦一個好用的BUG監(jiān)控工具 Fundebug。

交流

干貨系列文章匯總?cè)缦?,覺得不錯點個Star,歡迎 加群 互相學(xué)習(xí)。

https://github.com/qq44924588...

我是小智,公眾號「大遷世界」作者,對前端技術(shù)保持學(xué)習(xí)愛好者。我會經(jīng)常分享自己所學(xué)所看的干貨,在進(jìn)階的路上,共勉!

關(guān)注公眾號,后臺回復(fù)福利,即可看到福利,你懂的。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/104932.html

相關(guān)文章

  • 搞懂 JavsScript 異步 —? 事件輪詢

    showImg(https://segmentfault.com/img/bVbjYU7?w=2000&h=1333); 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! JavsScript 是一門單線程的編程語言,這就意味著一個時間里只能處理一件事,也就是說 JavaScript 引擎一次只能在一個線程里處理一條語句。 雖然單線程簡化了編程代碼,因為你不必太擔(dān)心并發(fā)引出的問...

    adam1q84 評論0 收藏0
  • 徹底搞懂JavaScript執(zhí)行機(jī)制

    摘要:徹底搞懂執(zhí)行機(jī)制首先我們大家都了解的是,是一門單線程語言,所以我們就可以得出是按照語句順序執(zhí)行的首先看這個顯然大家都知道結(jié)果,依次輸出,然而換一種這個時候再看代碼的順序執(zhí)行,輸出,,,。不過即使主線程為空,也是達(dá)不到的,根據(jù)標(biāo)準(zhǔn),最低是。 徹底搞懂JavaScript執(zhí)行機(jī)制 首先我們大家都了解的是,JavaScript 是一門單線程語言,所以我們就可以得出: JavaScript 是...

    hizengzeng 評論0 收藏0
  • 最后一次搞懂 Event Loop

    摘要:由于是單線程的,這些方法就會按順序被排列在一個單獨(dú)的地方,這個地方就是所謂執(zhí)行棧。事件隊列每次僅執(zhí)行一個任務(wù),在該任務(wù)執(zhí)行完畢之后,再執(zhí)行下一個任務(wù)。 Event Loop 是 JavaScript 異步編程的核心思想,也是前端進(jìn)階必須跨越的一關(guān)。同時,它又是面試的必考點,特別是在 Promise 出現(xiàn)之后,各種各樣的面試題層出不窮,花樣百出。這篇文章從現(xiàn)實生活中的例子入手,讓你徹底理解 E...

    gself 評論0 收藏0
  • 深入前端-徹底搞懂瀏覽器運(yùn)行機(jī)制

    摘要:當(dāng)這些異步任務(wù)發(fā)生的時候,它們將會被放入瀏覽器的事件任務(wù)隊列中去,等到運(yùn)行時執(zhí)行線程空閑時候才會按照隊列先進(jìn)先出的原則被一一執(zhí)行,但終究還是單線程。 瀏覽器是多進(jìn)程的 showImg(https://segmentfault.com/img/remote/1460000019706956?w=815&h=517); Browser進(jìn)程: 瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)、主控),只有一個。 負(fù)...

    YPHP 評論0 收藏0
  • 深入前端-徹底搞懂瀏覽器運(yùn)行機(jī)制

    摘要:當(dāng)這些異步任務(wù)發(fā)生的時候,它們將會被放入瀏覽器的事件任務(wù)隊列中去,等到運(yùn)行時執(zhí)行線程空閑時候才會按照隊列先進(jìn)先出的原則被一一執(zhí)行,但終究還是單線程。 瀏覽器是多進(jìn)程的 showImg(https://segmentfault.com/img/remote/1460000019706956?w=815&h=517); Browser進(jìn)程: 瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)、主控),只有一個。 負(fù)...

    Youngs 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<