一些名詞
JS引擎 — 一個讀取代碼并運行的引擎,沒有單一的“JS引擎”;,每個瀏覽器都有自己的引擎,如谷歌有V。
作用域 — 可以從中訪問變量的“區(qū)域”。
詞法作用域— 在詞法階段的作用域,換句話說,詞法作用域是由你在寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定的,因此當(dāng)詞法分析器處理代碼時會保持作用域不變。
塊作用域 — 由花括號{}創(chuàng)建的范圍
作用域鏈 — 函數(shù)可以上升到它的外部環(huán)境(詞法上)來搜索一個變量,它可以一直向上查找,直到它到達全局作用域。
同步 — 一次執(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),只能將元素推入并彈出頂部元素。 想想堆疊一個字形的塔樓; 你不能刪除中間塊,后進先出。
堆 — 變量存儲在內(nèi)存中。
調(diào)用堆棧 — 函數(shù)調(diào)用的隊列,它實現(xiàn)了堆棧數(shù)據(jù)類型,這意味著一次可以運行一個函數(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
有幾個點需要注意:
當(dāng)它被執(zhí)行時你期望發(fā)生什么? 是否發(fā)生錯誤,因為b在a之后聲明或者一切正常? 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還添加了:
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() }
如下步驟:
下面調(diào)用堆棧的執(zhí)行示意圖:
作用域及作用域鏈
在前面的示例中,所有內(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()
需要注意以下幾點:
打印結(jié)果如下:
myOtherVar: "global otherVar" myVar: "inside B"
執(zhí)行步驟:
上面提到每個新上下文會創(chuàng)建的外部引用,外部引用取決于函數(shù)在代碼中聲明的位置。
下面是執(zhí)行示意圖:
請記住,外部引用是單向的,它不是雙向關(guān)系。例如,函數(shù)b不能直接跳到函數(shù)c的上下文中并從那里獲取變量。
最好將它看作一個只能在一個方向上運行的鏈(范圍鏈)。
在上面的圖中,你可能注意到,函數(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
使用塊作用域可以使代碼更清晰,更安全,應(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
它記錄消息3
稍后,它會記錄消息2
setTimeout是一個 API,和大多數(shù)瀏覽器 API一樣,當(dāng)它被調(diào)用時,它會向瀏覽器發(fā)送一些數(shù)據(jù)和回調(diào)。我們這邊是延遲一秒打印 Message 2。
調(diào)用完setTimeout 后,我們的代碼繼續(xù)運行,沒有暫停,打印 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可以提高代碼運行速度,是一個很好的工具。
我們可以使用閉包實現(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,花了大量的時間進行l(wèi)og 調(diào)試,這邊順便給大家推薦一個好用的BUG監(jiān)控工具 Fundebug。
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com