JavaScript在瀏覽器中的執(zhí)行機(jī)制
既然說(shuō)到了 JavaScript,那么就會(huì)繞不過(guò)去執(zhí)行上下文,只有理解了執(zhí)行上下文才能更好的理解 JavaScript 本身,比如變量提升、棧溢出、作用域和閉包等。
不過(guò)本部分不是專門(mén)講解 JavaScript 的,所有不會(huì)對(duì) JavaScript 本身做過(guò)多介紹,主要從 JavaScript 的執(zhí)行順序開(kāi)始介紹一下 JavaScript 是怎樣運(yùn)行的。
首先先看一下下面的代碼:
showName() console.log(myname) var myname = '掘金' function showName() { console.log('function showName run') }
作為前端大家都知道上面的代碼是同步的會(huì)順序執(zhí)行,那么輸出應(yīng)該是:
- 當(dāng)執(zhí)行第一行時(shí)因?yàn)?nbsp;
showName
方法還沒(méi)有定義應(yīng)該會(huì)報(bào)錯(cuò)。 - 同樣執(zhí)行第二行時(shí)因?yàn)樽兞?nbsp;
name
也沒(méi)有定義所有也會(huì)報(bào)錯(cuò)。
但是如下圖執(zhí)行結(jié)果跟我們想的并不一樣:
第1行輸出 “function showName run”,第2行輸出 “undefined”,這和前面想象中的順序執(zhí)行有點(diǎn)不一樣?。?/p>
上面結(jié)果可以看出函數(shù)或者變量可以在定義之前使用,如果沒(méi)有定義代碼還能正常執(zhí)行嗎,可以參考下面代碼去掉第三行 myName
的定義:
showName() console.log(myname) function showName() { console.log('function showName run') }
使用了未定義的變量——執(zhí)行報(bào)錯(cuò)
從上面兩段代碼的執(zhí)行結(jié)果來(lái)看,我們可以得出如下三個(gè)結(jié)論。
- 在執(zhí)行過(guò)程中,若使用了未聲明的變量,那么JavaScript執(zhí)行會(huì)報(bào)錯(cuò)。
- 在一個(gè)變量定義之前使用它,不會(huì)出錯(cuò),但是該變量的值會(huì)為undefined,而不是定義時(shí)的值。
- 在一個(gè)函數(shù)定義之前使用它,不會(huì)出錯(cuò),且函數(shù)能正確執(zhí)行。
第一個(gè)結(jié)論很好理解,因?yàn)樽兞繘](méi)有定義,這樣在執(zhí)行JavaScript代碼時(shí),就找不到該變量,所以JavaScript會(huì)拋出錯(cuò)誤。
但是對(duì)于第二個(gè)和第三個(gè)結(jié)論,就挺讓人費(fèi)解的,要解決這兩個(gè)問(wèn)題就要明白接下來(lái)我們要介紹的內(nèi)容-變量提升。
變量提升
在正式了解變量提升之前,我們需要先理解兩個(gè) JavaScript 概念聲明和賦值。
var myName = "掘金"
這段代碼可以這樣拆解看:
var myName //聲明部分 myName = "掘金" //賦值部分
上面說(shuō)明了變量的聲明和賦值,接下來(lái)說(shuō)一下函數(shù)的聲明和賦值:
function foo(){ console.log('foo') } var bar = function(){ console.log('bar') }
第一個(gè)函數(shù)foo是一個(gè)完整的函數(shù)聲明,也就是說(shuō)沒(méi)有涉及到賦值操作;第二個(gè)函數(shù)是先聲明變量bar,再把function(){console.log('bar')}賦值給bar。為了直觀理解,你可以參考下圖:
通過(guò)上面我們了解了變量和函數(shù)的聲明和賦值了,接下來(lái)說(shuō)一說(shuō)什么是變量提升。
所謂的變量提升就是指 JavaScript 代碼在執(zhí)行過(guò)程中, JavaScript 引擎把變量和函數(shù)的聲明部分提升到代碼開(kāi)頭的”行為“。變量提升后,會(huì)給變量設(shè)置默認(rèn)值,這個(gè)默認(rèn)值就是我們熟悉的 undefined
。
具體變化如下代碼:
/* * 變量提升部分 */ // 把變量 myname提升到開(kāi)頭, // 同時(shí)給myname賦值為undefined var myname = undefined // 把函數(shù)showName提升到開(kāi)頭 function showName() { console.log('function showName run'); } /* * 可執(zhí)行代碼部分 */ showName() console.log(myname) // 去掉var聲明部分,保留賦值語(yǔ)句 myname = '掘金'
可以通過(guò)下圖更直觀的感受:
從圖中可以看出,對(duì)原來(lái)的代碼主要做了兩處調(diào)整:
- 第一處是把聲明的部分都提升到了代碼開(kāi)頭,如變量
myName
和函數(shù)showName
,并給變量設(shè)置默認(rèn)值undefined
; - 第二處是移除原本聲明的變量和函數(shù),如
var myName = '掘金'
的語(yǔ)句,移除了 var 聲明,整個(gè)移除showName
的函數(shù)聲明。
通過(guò)這兩步,就可以實(shí)現(xiàn)變量提升的效果。你也可以執(zhí)行這段模擬變量提升的代碼,其輸出結(jié)果和第一段代碼應(yīng)該是完全一樣的。
通過(guò)這段模擬的變量提升代碼,明白了可以在定義之前使用變量或者函數(shù)的原因——函數(shù)和變量在執(zhí)行之前都提升到了代碼開(kāi)頭。
let和const會(huì)變量提升嗎?
我們執(zhí)行下面代碼進(jìn)行嘗試:
console.log(myName) const myName = '掘金'
上圖的報(bào)錯(cuò)為:ReferenceError: Cannot access 'variable' before initialization。和未定義的的報(bào)錯(cuò)不同,未定義報(bào)錯(cuò)如下
console.log(myName1) const myName = '掘金'
由此可見(jiàn),用 var 定義的變量可以在聲明之前被訪問(wèn)而不報(bào)錯(cuò),但是用 let
或 const
定義的變量卻不行。
由 let
或 const
定義的變量提升時(shí)不會(huì)默認(rèn)初始化,所以在聲明之前訪問(wèn)會(huì)報(bào)錯(cuò):ReferenceError: Cannot access 'variable' before initialization。
然而由 var
定義的變量提升時(shí)會(huì)被初始化為默認(rèn)值 undefined
,所以在聲明之前訪問(wèn)會(huì)得到 undefined
。 這里面就是涉及到了一個(gè)概念 暫時(shí)性死區(qū)
,let
/const
定義的變量被提升卻無(wú)法正常訪問(wèn),是因?yàn)榇嬖?strong>暫時(shí)性死區(qū)(Temporal Dead Zone) 。
JavaScript 代碼的執(zhí)行順序
通過(guò)上面介紹變量提升,你可能會(huì)覺(jué)得變量和函數(shù)會(huì)在物理層面中被移動(dòng)到代碼最前面,就是我們圖中展示的那樣,但是,實(shí)際上變量和函數(shù)聲明在代碼里的位置并不會(huì)改變,而是在編譯階段被 JavaScript 引擎放入到內(nèi)存中,你沒(méi)看錯(cuò),一段 JavaScript 代碼在被執(zhí)行前需要 JavaScript 引擎編譯,編譯完成后才會(huì)被執(zhí)行,如下圖所示:
1.編譯階段
編譯和變量提升有什么關(guān)系呢?我們可以從下面兩部分代碼來(lái)看:
第一部分:變量提升部分的代碼。
var?myName?=?undefined function?showName()?{ ????console.log('function showName run'); }
第二部分:執(zhí)行部分的代碼。
showName() console.log(myName) myName?=?'掘金'
可以把JavaScript的執(zhí)行流程細(xì)化,如下圖所示:
從上圖可以看出,輸入一段代碼,經(jīng)過(guò)編譯后,會(huì)生成兩部分內(nèi)容:執(zhí)行上下文(Execution context)和可執(zhí)行代碼。
執(zhí)行上下文是JavaScript執(zhí)行一段代碼時(shí)的運(yùn)行環(huán)境,比如調(diào)用一個(gè)函數(shù),就會(huì)進(jìn)入這個(gè)函數(shù)的執(zhí)行上下文,確定該函數(shù)在執(zhí)行期間用到的諸如this、變量、對(duì)象以及函數(shù)等。
可以用下面的偽代碼簡(jiǎn)單的理解環(huán)境變量對(duì)象:
VariableEnvironment: myname -> undefined, showName ->function : {console.log(myname)
接下來(lái)看一下生成環(huán)境變量的過(guò)程:
showName() console.log(myName) var myName = '掘金' function showName() { console.log('function showName run'); }
- 第1行和第2行,由于這兩行代碼不是聲明操作,所以JavaScript引擎不會(huì)做任何處理;
- 第3行,由于這行是經(jīng)過(guò)var聲明的,因此JavaScript引擎將在環(huán)境對(duì)象中創(chuàng)建一個(gè)名為
myName
的屬性,并使用undefined
對(duì)其初始化; - 第4行,JavaScript引擎發(fā)現(xiàn)了一個(gè)通過(guò) function 定義的函數(shù),所以它將函數(shù)定義存儲(chǔ)到堆(HEAP)中,并在環(huán)境對(duì)象中創(chuàng)建一個(gè)showName的屬性,然后將該屬性值指向堆中函數(shù)的位置。
這樣就生成了變量環(huán)境對(duì)象。接下來(lái)JavaScript引擎會(huì)把聲明以外的代碼編譯為字節(jié)碼,可以類比如下的偽代碼:
showName() console.log(myname) myname = '掘金'
好了,現(xiàn)在有了執(zhí)行上下文和可執(zhí)行代碼了,那么接下來(lái)就到了執(zhí)行階段了。
2. 執(zhí)行階段
JavaScript引擎開(kāi)始執(zhí)行“可執(zhí)行代碼”,按照順序一行一行地執(zhí)行。下面我們就來(lái)一行一行分析下這個(gè)執(zhí)行過(guò)程:
- 當(dāng)執(zhí)行到
showName
函數(shù)時(shí),JavaScript 引擎便開(kāi)始在變量環(huán)境對(duì)象中查找該函數(shù),由于變量環(huán)境對(duì)象中存在該函數(shù)的引用,所以 JavaScript 引擎便開(kāi)始執(zhí)行該函數(shù),并輸出“函數(shù)showName
被執(zhí)行”結(jié)果。 - 接下來(lái)打印
myName
信息,JavaScript 引擎繼續(xù)在變量環(huán)境對(duì)象中查找該對(duì)象,由于變量環(huán)境存在myName
變量,并且其值為undefined
,所以這時(shí)候就輸出undefined
。 - 接下來(lái)執(zhí)行第3行,把“掘金”賦給
myName
變量,賦值后變量環(huán)境中的myname
屬性值改變?yōu)?ldquo;掘金”,變量環(huán)境如下所示:
VariableEnvironment: ?????myname?->?"掘金",? ?????showName?->function?:?{console.log(myName)
代碼中出現(xiàn)相同的變量或者函數(shù)怎么辦?
現(xiàn)在你已經(jīng)知道了,在執(zhí)行一段JavaScript代碼之前,會(huì)編譯代碼,并將代碼中的函數(shù)和變量保存到執(zhí)行上下文的變量環(huán)境中,那么如果代碼中出現(xiàn)了重名的函數(shù)或者變量,JavaScript引擎會(huì)如何處理?
我們先看下面這樣一段代碼:
function?showName()?{ ????console.log('沸點(diǎn)'); } showName(); function?showName()?{ ????console.log('掘金'); } showName();?
分析這兩次調(diào)用打印出來(lái)的值是什么嗎?
完整執(zhí)行流程:
- 首先是編譯階段。遇到了第一個(gè)
showName
函數(shù),會(huì)將該函數(shù)體存放到變量環(huán)境中。接下來(lái)是第二個(gè)showName
函數(shù),繼續(xù)存放至變量環(huán)境中,但是變量環(huán)境中已經(jīng)存在一個(gè)showName
函數(shù)了,此時(shí),第二個(gè)showName
函數(shù)會(huì)將第一個(gè)showName
函數(shù)覆蓋掉。這樣變量環(huán)境中就只存在第二個(gè)showName
函數(shù)了。 - 接下來(lái)是執(zhí)行階段。先執(zhí)行第一個(gè)
showName
函數(shù),但由于是從變量環(huán)境中查找showName
函數(shù),而變量環(huán)境中只保存了第二個(gè)showNam
e函數(shù),所以最終調(diào)用的是第二個(gè)函數(shù),打印的內(nèi)容是“掘金”。第二次執(zhí)行showName函數(shù)也是走同樣的流程,所以輸出的結(jié)果也是“掘金”。
綜上所述,一段代碼如果定義了兩個(gè)相同名字的函數(shù),那么最終生效的是最后一個(gè)函數(shù)。
總結(jié)
好了,今天就到這里,下面我來(lái)簡(jiǎn)單總結(jié)下今天的主要內(nèi)容:
- JavaScript 代碼執(zhí)行過(guò)程中,需要先做變量提升,而之所以需要實(shí)現(xiàn)變量提升,是因?yàn)?JavaScript 代碼在執(zhí)行之前需要先編譯。
- 在編譯階段,變量和函數(shù)會(huì)被存放到變量環(huán)境中,變量的默認(rèn)值會(huì)被設(shè)置為
undefined
;在代碼執(zhí)行階段,JavaScript 引擎會(huì)從變量環(huán)境中去查找自定義的變量和函數(shù)。 - 如果在編譯階段,存在兩個(gè)相同的函數(shù),那么最終存放在變量環(huán)境中的是最后定義的那個(gè),這是因?yàn)楹蠖x的會(huì)覆蓋掉之前定義的。
以上就是今天所講的主要內(nèi)容,當(dāng)然,學(xué)習(xí)這些內(nèi)容并不是讓你掌握一些 JavaScript 小技巧,其主要目的是讓你清楚JavaScript的執(zhí)行機(jī)制:先編譯,再執(zhí)行。
以上就是JavaScript在瀏覽器中的執(zhí)行機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于JavaScript執(zhí)行機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
bootstrap的3級(jí)菜單樣式,支持母版頁(yè)保留打開(kāi)狀態(tài)實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇bootstrap的3級(jí)菜單樣式,支持母版頁(yè)保留打開(kāi)狀態(tài)實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11JavaScript如何實(shí)現(xiàn)圖片處理與合成
這篇文章主要介紹了JavaScript如何實(shí)現(xiàn)圖片處理與合成,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05微信小程序自定義tab實(shí)現(xiàn)多層tab嵌套功能
微信小程序非?;鸨?,今天腳本之家小編給大家?guī)?lái)的微信小程序自定義tab實(shí)現(xiàn)多層tab嵌套功能以及項(xiàng)目中遇到的問(wèn)題及解決方法,感興趣的朋友一起看看吧2018-06-06XML文件轉(zhuǎn)化成NSData對(duì)象的方法
這篇文章主要介紹了XML文件轉(zhuǎn)化成NSData對(duì)象的方法,需要的朋友可以參考下2015-08-08fixedBox固定div漂浮代碼支持ie6以上大部分主流瀏覽器
本例為大家分享的是fixedBox固定div漂浮代碼支持ie6以上大部分瀏覽器,需要的朋友可以參考下2014-06-06原生js實(shí)現(xiàn)一個(gè)放大鏡效果超詳細(xì)
這篇文章主要介紹了原生js實(shí)現(xiàn)一個(gè)放大鏡效果超詳細(xì),文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09JS控制鼠標(biāo)拒絕點(diǎn)擊某一按鈕的實(shí)例
下面小編就為大家分享一篇JS控制鼠標(biāo)拒絕點(diǎn)擊某一按鈕的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12