摘要:同步一次執行一件事,同步引擎一次只執行一行,是同步的。調用函數將其推入堆棧并從函數返回將其彈出堆棧。執行上下文當函數放入到調用堆棧時由創建的環境。執行結果它會立即被推到回調隊列,但它仍然會等待調用堆棧為空才會執行。
為了保證可讀性,本文采用意譯而非直譯。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
一些名詞JS引擎 — 一個讀取代碼并運行的引擎,沒有單一的“JS引擎”;,每個瀏覽器都有自己的引擎,如谷歌有V。
作用域 — 可以從中訪問變量的“區域”。
詞法作用域— 在詞法階段的作用域,換句話說,詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪里來決定的,因此當詞法分析器處理代碼時會保持作用域不變。
塊作用域 — 由花括號{}創建的范圍
作用域鏈 — 函數可以上升到它的外部環境(詞法上)來搜索一個變量,它可以一直向上查找,直到它到達全局作用域。
同步 — 一次執行一件事, “同步”引擎一次只執行一行,JavaScript是同步的。
異步 — 同時做多個事,JS通過瀏覽器API模擬異步行為
事件循環(Event Loop) - 瀏覽器API完成函數調用的過程,將回調函數推送到回調隊列(callback queue),然后當堆棧為空時,它將回調函數推送到調用堆棧。
堆棧 —一種數據結構,只能將元素推入并彈出頂部元素。 想想堆疊一個字形的塔樓; 你不能刪除中間塊,后進先出。
堆 — 變量存儲在內存中。
調用堆棧 — 函數調用的隊列,它實現了堆棧數據類型,這意味著一次可以運行一個函數。 調用函數將其推入堆棧并從函數返回將其彈出堆棧。
執行上下文 — 當函數放入到調用堆棧時由JS創建的環境。
閉包 — 當在另一個函數內創建一個函數時,它“記住”它在以后調用時創建的環境。
垃圾收集 — 當內存中的變量被自動刪除時,因為它不再使用,引擎要處理掉它。
變量的提升— 當變量內存沒有賦值時會被提升到全局的頂部并設置為undefined。
this —由JavaScript為每個新的執行上下文自動創建的變量/關鍵字。
調用堆棧(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
有幾個點需要注意:
變量聲明的位置(一個在上,一個在下)
函數a調用下面定義的函數b, 函數b調用函數c
當它被執行時你期望發生什么? 是否發生錯誤,因為b在a之后聲明或者一切正常? console.log 打印的變量又是怎么樣?
以下是打印結果:
"myVar" undefined "myOtherVar" 10 "Hello world!"
來分解一下上述的執行步驟。
1. 變量和函數聲明(創建階段)第一步是在內存中為所有變量和函數分配空間。 但請注意,除了undefined之外,尚未為變量分配值。 因此,myVar在被打印時的值是undefined,因為JS引擎從頂部開始逐行執行代碼。
函數與變量不一樣,函數可以一次聲明和初始化,這意味著它們可以在任何地方被調用。
所以以上代碼看起來像這樣子:
var myOtherVar = undefined var myVar = undefined function a() {...} function b() {...} function c() {...}
這些都存在于JS創建的全局上下文中,因為它位于全局空間中。
在全局上下文中,JS還添加了:
全局對象(瀏覽器中是 window 對象,NodeJs 中是 global 對象)
this 指向全局對象
2. 執行接下來,JS 引擎會逐行執行代碼。
myOtherVar = 10在全局上下文中,myOtherVar被賦值為10
已經創建了所有函數,下一步是執行函數 a()
每次調用函數時,都會為該函數創建一個新的上下文(重復步驟1),并將其放入調用堆棧。
function a() { console.log("myVar", myVar) b() }
如下步驟:
創建新的函數上下文
a 函數里面沒有聲明變量和函數
函數內部創建了 this 并指向全局對象(window)
接著引用了外部變量 myVar,myVar 屬于全局作用域的。
接著調用函數 b ,函數b的過程跟 a一樣,這里不做分析。
下面調用堆棧的執行示意圖:
創建全局上下文,全局變量和函數。
每個函數的調用,會創建一個上下文,外部環境的引用及 this。
函數執行結束后會從堆棧中彈出,并且它的執行上下文被垃圾收集回收(閉包除外)。
當調用堆棧為空時,它將從事件隊列中獲取事件。
作用域及作用域鏈在前面的示例中,所有內容都是全局作用域的,這意味著我們可以從代碼中的任何位置訪問它。 現在,介紹下私有作用域以及如何定義作用域。
函數/詞法作用域考慮如下代碼:
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()
需要注意以下幾點:
全局作用域和函數內部都聲明了變量
函數c現在在函數b中聲明
打印結果如下:
myOtherVar: "global otherVar" myVar: "inside B"
執行步驟:
全局創建和聲明 - 創建內存中的所有函數和變量以及全局對象和 this
執行 - 它逐行讀取代碼,給變量賦值,并執行函數a
函數a創建一個新的上下文并被放入堆棧,在上下文中創建變量myOtherVar,然后調用函數b
函數b 也會創建一個新的上下文,同樣也被放入堆棧中
5,函數b的上下文中創建了 myVar 變量,并聲明函數c
上面提到每個新上下文會創建的外部引用,外部引用取決于函數在代碼中聲明的位置。
函數b試圖打印myOtherVar,但這個變量并不存在于函數b中,函數b 就會使用它的外部引用上作用域鏈向上找。由于函數b是全局聲明的,而不是在函數a內部聲明的,所以它使用全局變量myOtherVar。
函數c執行步驟一樣。由于函數c本身沒有變量myVar,所以它它通過作用域鏈向上找,也就是函數b,因為myVar是函數b內部聲明過。
下面是執行示意圖:
請記住,外部引用是單向的,它不是雙向關系。例如,函數b不能直接跳到函數c的上下文中并從那里獲取變量。
最好將它看作一個只能在一個方向上運行的鏈(范圍鏈)。
a -> global
c -> b -> global
在上面的圖中,你可能注意到,函數是創建新作用域的一種方式。(除了全局作用域)然而,還有另一種方法可以創建新的作用域,就是塊作用域。
塊作用域下面代碼中,我們有兩個變量和兩個循環,在循環重新聲明相同的變量,會打印什么(反正我是做錯了)?
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()
打印結果:
i = 10 j = 99
第一個循環覆蓋了var i,對于不知情的開發人員來說,這可能會導致bug。
第二個循環,每次迭代創建了自己作用域和變量。 這是因為它使用let關鍵字,它與var相同,只是let有自己的塊作用域。 另一個關鍵字是const,它與let相同,但const常量且無法更改(指內存地址)。
塊作用域由大括號 {} 創建的作用域
再看一個例子:
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()
打印結果:
a = 9000 b = 11 ReferenceError: blockedVar is not defined
a是塊作用域,但它在函數中,而不是嵌套的,本例中使用var是一樣的。
對于塊作用域的變量,它的行為類似于函數,注意var b可以在外部訪問,但是const blockedVar不能。
在塊內部,從作用域鏈向上找到 a 并將let a更改為9000。
使用塊作用域可以使代碼更清晰,更安全,應該盡可能地使用它。
事件循環(Event Loop)接下來看看事件循環。 這是回調,事件和瀏覽器API工作的地方
我們沒有過多討論的事情是堆,也叫全局內存。它是變量存儲的地方。由于了解JS引擎是如何實現其數據存儲的實際用途并不多,所以我們不在這里討論它。
來個異步代碼:
function logMessage2 () { console.log("Message 2") } console.log("Message 1") setTimeout(logMessage2, 1000) console.log("Message 3")
上述代碼主要是將一些 message 打印到控制臺。 利用setTimeout函數來延遲一條消息。 我們知道js是同步,來看看輸出結果
Message 1 Message 3 Message 2
打印 Message 1
調用 setTimeout
打印 Message 3
打印 Message 2
它記錄消息3
稍后,它會記錄消息2
setTimeout是一個 API,和大多數瀏覽器 API一樣,當它被調用時,它會向瀏覽器發送一些數據和回調。我們這邊是延遲一秒打印 Message 2。
調用完setTimeout 后,我們的代碼繼續運行,沒有暫停,打印 Message 3 并執行一些必須先執行的操作。
瀏覽器等待一秒鐘,它就會將數據傳遞給我們的回調函數并將其添加到事件/回調隊列中( event/callback queue)。 然后停留在隊列中,只有當調用堆棧(call stack)為空時才會被壓入堆棧。
代碼示例要熟悉JS引擎,最好的方法就是使用它,再來些有意義的例子。
簡單的閉包這個例子中 有一個返回函數的函數,并在返回的函數中使用外部的變量, 這稱為閉包。
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塊代碼
我們使用無限循環將將調用堆棧塞滿,會發生什么,回調隊列被會阻塞,因為只能在調用堆棧為空時添加回調隊列。
function blockingCode() { const startTime = new Date().getSeconds() // 延遲函數250毫秒 setTimeout(function() { const calledAt = new Date().getSeconds() const diff = calledAt - startTime // 打印調用此函數所需的時間 console.log(`Callback called after: ${diff} seconds`) }, 250) // 用循環阻塞堆棧2秒鐘 while(true) { const currentTime = new Date().getSeconds() // 2 秒后退出 if(currentTime - startTime >= 2) break } } blockingCode() // "Callback called after: 2 seconds"
我們試圖在250毫秒之后調用一個函數,但因為我們的循環阻塞了堆棧所花了兩秒鐘,所以回調函數實際是兩秒后才會執行,這是JavaScript應用程序中的常見錯誤。
setTimeout不能保證在設置的時間之后調用函數。相反,更好的描述是,在至少經過這段時間之后調用這個函數。
延遲函數當 setTimeout 的設置為0,情況是怎么樣?
function defer () { setTimeout(() => console.log("timeout with 0 delay!"), 0) console.log("after timeout") console.log("last log") } defer()
你可能期望它被立即調用,但是,事實并非如此。
執行結果:
after timeout last log timeout with 0 delay!
它會立即被推到回調隊列,但它仍然會等待調用堆棧為空才會執行。
用閉包來緩存Memoization是緩存函數調用結果的過程。
例如,有一個添加兩個數字的函數add。調用add(1,2)返回3,當再次使用相同的參數add(1,2)調用它,這次不是重新計算,而是記住1 + 2是3的結果并直接返回對應的結果。 Memoization可以提高代碼運行速度,是一個很好的工具。
我們可以使用閉包實現一個簡單的memoize函數。
// 緩存函數,接收一個函數 const memoize = (func) => { // 緩存對象 // keys 是 arguments, values are results const cache = {} // 返回一個新的函數 // it remembers the cache object & func (closure) // ...args is any number of arguments return (...args) => { // 將參數轉換為字符串,以便我們可以存儲它 const argStr = JSON.stringify(args) // 如果已經存,則打印 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))
執行結果:
cache {} false first add call: 3 cache { "[1,2]": 3 } true second add call 3
第一次 add 方法,緩存對象是空的,它調用我們的傳入函數來獲取值3.然后它將args/value鍵值對存儲在緩存對象中。
在第二次調用中,緩存中已經有了,查找到并返回值。
對于add函數來說,有無緩存看起來無關緊要,甚至效率更低,但是對于一些復雜的計算,它可以節省很多時間。這個示例并不是一個完美的緩存示例,而是閉包的實際應用。
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
交流干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104932.html
showImg(https://segmentfault.com/img/bVbjYU7?w=2000&h=1333); 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! JavsScript 是一門單線程的編程語言,這就意味著一個時間里只能處理一件事,也就是說 JavaScript 引擎一次只能在一個線程里處理一條語句。 雖然單線程簡化了編程代碼,因為你不必太擔心并發引出的問...
摘要:徹底搞懂執行機制首先我們大家都了解的是,是一門單線程語言,所以我們就可以得出是按照語句順序執行的首先看這個顯然大家都知道結果,依次輸出,然而換一種這個時候再看代碼的順序執行,輸出,,,。不過即使主線程為空,也是達不到的,根據標準,最低是。 徹底搞懂JavaScript執行機制 首先我們大家都了解的是,JavaScript 是一門單線程語言,所以我們就可以得出: JavaScript 是...
摘要:由于是單線程的,這些方法就會按順序被排列在一個單獨的地方,這個地方就是所謂執行棧。事件隊列每次僅執行一個任務,在該任務執行完畢之后,再執行下一個任務。 Event Loop 是 JavaScript 異步編程的核心思想,也是前端進階必須跨越的一關。同時,它又是面試的必考點,特別是在 Promise 出現之后,各種各樣的面試題層出不窮,花樣百出。這篇文章從現實生活中的例子入手,讓你徹底理解 E...
摘要:當這些異步任務發生的時候,它們將會被放入瀏覽器的事件任務隊列中去,等到運行時執行線程空閑時候才會按照隊列先進先出的原則被一一執行,但終究還是單線程。 瀏覽器是多進程的 showImg(https://segmentfault.com/img/remote/1460000019706956?w=815&h=517); Browser進程: 瀏覽器的主進程(負責協調、主控),只有一個。 負...
摘要:當這些異步任務發生的時候,它們將會被放入瀏覽器的事件任務隊列中去,等到運行時執行線程空閑時候才會按照隊列先進先出的原則被一一執行,但終究還是單線程。 瀏覽器是多進程的 showImg(https://segmentfault.com/img/remote/1460000019706956?w=815&h=517); Browser進程: 瀏覽器的主進程(負責協調、主控),只有一個。 負...
閱讀 3758·2023-04-25 20:00
閱讀 3109·2021-09-22 15:09
閱讀 505·2021-08-25 09:40
閱讀 3412·2021-07-26 23:38
閱讀 2201·2019-08-30 15:53
閱讀 1097·2019-08-30 13:46
閱讀 2788·2019-08-29 16:44
閱讀 2043·2019-08-29 15:32