溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

javascript變量提升的相關(guān)知識有哪些

發(fā)布時間:2022-02-24 09:35:08 來源:億速云 閱讀:141 作者:小新 欄目:web開發(fā)

這篇文章將為大家詳細(xì)講解有關(guān)javascript變量提升的相關(guān)知識有哪些,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

咱們先看段代碼,你覺得下面這段代碼輸出的結(jié)果是什么?

showName()
console.log(myname)
var myname = '極客時間'
function showName() {
    console.log('函數(shù)showName被執(zhí)行');
}

使用過 JavaScript 開發(fā)的程序員應(yīng)該都知道,JavaScript 是按順序執(zhí)行的。若按照這個邏輯來理解的話,那么:

  • 當(dāng)執(zhí)行到第 1 行的時候,由于函數(shù) showName 還沒有定義,所以執(zhí)行應(yīng)該會報錯;

  • 同樣執(zhí)行第 2 行的時候,由于變量 myname 也未定義,所以同樣也會報錯。

然而實際執(zhí)行結(jié)果卻并非如此, 如下圖:
javascript變量提升的相關(guān)知識有哪些
第 1 行輸出“函數(shù) showName 被執(zhí)行”,第 2 行輸出“undefined”,這和前面想象中的順序執(zhí)行有點不一樣?。?/p>

通過上面的執(zhí)行結(jié)果,你應(yīng)該已經(jīng)知道了函數(shù)或者變量可以在定義之前使用,那如果使用沒有定義的變量或者函數(shù),JavaScript 代碼還能繼續(xù)執(zhí)行嗎?為了驗證這點,我們可以刪除第 3 行變量 myname 的定義,如下所示:

showName()
console.log(myname)
function showName() {
    console.log('函數(shù)showName被執(zhí)行');
}

然后再次執(zhí)行這段代碼時,JavaScript 引擎就會報錯,結(jié)果如下:
javascript變量提升的相關(guān)知識有哪些
從上面兩段代碼的執(zhí)行結(jié)果來看,我們可以得出如下三個結(jié)論:

  • 在執(zhí)行過程中,若使用了未聲明的變量,那么 JavaScript 執(zhí)行會報錯。

  • 在一個變量定義之前使用它,不會出錯,但是該變量的值會為 undefined,而不是定義時的值。

  • 在一個函數(shù)定義之前使用它,不會出錯,且函數(shù)能正確執(zhí)行。

第一個結(jié)論很好理解,因為變量沒有定義,這樣在執(zhí)行 JavaScript 代碼時,就找不到該變量,所以 JavaScript 會拋出錯誤。

但是對于第二個和第三個結(jié)論,就挺讓人費解的:

  • 變量和函數(shù)為什么能在其定義之前使用?這似乎表明 JavaScript 代碼并不是一行一行執(zhí)行的。

  • 同樣的方式,變量和函數(shù)的處理結(jié)果為什么不一樣?比如上面的執(zhí)行結(jié)果,提前使用的 showName 函數(shù)能打印出來完整結(jié)果,但是提前使用的 myname 變量值卻是 undefined,而不是定義時使用的“極客時間”這個值。

變量提升(Hoisting)

要解釋這兩個問題,你就需要先了解下什么是變量提升。

不過在介紹變量提升之前,我們先通過下面這段代碼,來看看什么是 JavaScript 中的聲明和賦值。

var myname = '極客時間'

這段代碼你可以把它看成是兩行代碼組成的:

var myname    //聲明部分
myname = '極客時間'  //賦值部分

如下圖所示:
javascript變量提升的相關(guān)知識有哪些
上面是變量的聲明和賦值,那接下來我們再來看看函數(shù)的聲明和賦值,結(jié)合下面這段代碼:

function foo(){
  console.log('foo')
}

var bar = function(){
  console.log('bar')
}

第一個函數(shù) foo 是一個完整的函數(shù)聲明,也就是說沒有涉及到賦值操作;第二個函數(shù)是先聲明變量 bar,再把function(){console.log(‘bar’)}賦值給 bar。為了直觀理解,你可以參考下圖:
javascript變量提升的相關(guān)知識有哪些
好了,理解了聲明和賦值操作,那接下來我們就可以聊聊什么是變量提升了。

所謂的變量提升,是指在 JavaScript 代碼執(zhí)行過程中,JavaScript 引擎把變量的聲明部分和函數(shù)的聲明部分提升到代碼開頭的“行為”。變量被提升后,會給變量設(shè)置默認(rèn)值,這個默認(rèn)值就是我們熟悉的 undefined。

下面我們來模擬下實現(xiàn):

/*
* 變量提升部分
*/// 把變量 myname提升到開頭,// 同時給myname賦值為undefinedvar myname = undefined// 把函數(shù)showName提升到開頭function showName() {
    console.log('showName被調(diào)用');}/*
 * 可執(zhí)行代碼部分
*/showName()console.log(myname)// 去掉var聲明部分,保留賦值語句myname = '極客時間'

為了模擬變量提升的效果,我們對代碼做了以下調(diào)整,如下圖:
javascript變量提升的相關(guān)知識有哪些
從圖中可以看出,對原來的代碼主要做了兩處調(diào)整:

  • 第一處是把聲明的部分都提升到了代碼開頭,如變量 myname 和函數(shù) showName,并給變量設(shè)置默認(rèn)值 undefined;

  • 第二處是移除原本聲明的變量和函數(shù),如var myname = '極客時間’的語句,移除了 var 聲明,整個移除 showName 的函數(shù)聲明。

通過這兩步,就可以實現(xiàn)變量提升的效果。你也可以執(zhí)行這段模擬變量提升的代碼,其輸出結(jié)果和第一段代碼應(yīng)該是完全一樣的。

通過這段模擬的變量提升代碼,相信你已經(jīng)明白了可以在定義之前使用變量或者函數(shù)的原因——函數(shù)和變量在執(zhí)行之前都提升到了代碼開頭

JavaScript 代碼的執(zhí)行流程

從概念的字面意義上來看,“變量提升”意味著變量和函數(shù)的聲明會在物理層面移動到代碼的最前面,正如我們所模擬的那樣。但,這并不準(zhǔn)確。實際上變量和函數(shù)聲明在代碼里的位置是不會改變的,而且是在編譯階段被 JavaScript 引擎放入內(nèi)存中。對,你沒聽錯,一段 JavaScript 代碼在執(zhí)行之前需要被 JavaScript 引擎編譯,編譯完成之后,才會進(jìn)入執(zhí)行階段。大致流程你可以參考下圖:
javascript變量提升的相關(guān)知識有哪些

1. 編譯階段

那么編譯階段和變量提升存在什么關(guān)系呢?

為了搞清楚這個問題,我們還是回過頭來看上面那段模擬變量提升的代碼,為了方便介紹,可以把這段代碼分成兩部分。

第一部分:變量提升部分的代碼。

var myname = undefined
function showName() {
    console.log('函數(shù)showName被執(zhí)行');
}

第二部分:執(zhí)行部分的代碼。

showName()
console.log(myname)
myname = '極客時間'

下面我們就可以把 JavaScript 的執(zhí)行流程細(xì)化,如下圖所示:

javascript變量提升的相關(guān)知識有哪些
從上圖可以看出,輸入一段代碼,經(jīng)過編譯后,會生成兩部分內(nèi)容:執(zhí)行上下文(Execution context)和可執(zhí)行代碼。

執(zhí)行上下文是 JavaScript 執(zhí)行一段代碼時的運(yùn)行環(huán)境,比如調(diào)用一個函數(shù),就會進(jìn)入這個函數(shù)的執(zhí)行上下文,確定該函數(shù)在執(zhí)行期間用到的諸如 this、變量、對象以及函數(shù)等。

關(guān)于執(zhí)行上下文的細(xì)節(jié),我會在下一篇文章《08 | 調(diào)用棧:為什么 JavaScript 代碼會出現(xiàn)棧溢出?》做詳細(xì)介紹,現(xiàn)在你只需要知道,在執(zhí)行上下文中存在一個變量環(huán)境的對象(Viriable Environment),該對象中保存了變量提升的內(nèi)容,比如上面代碼中的變量 myname 和函數(shù) showName,都保存在該對象中。

你可以簡單地把變量環(huán)境對象看成是如下結(jié)構(gòu):

VariableEnvironment:
     myname -> undefined, 
     showName ->function : {console.log(myname)

了解完變量環(huán)境對象的結(jié)構(gòu)后,接下來,我們再結(jié)合下面這段代碼來分析下是如何生成變量環(huán)境對象的。

showName()
console.log(myname)
var myname = '極客時間'
function showName() {
    console.log('函數(shù)showName被執(zhí)行');
}

我們可以一行一行來分析上述代碼:

  • 第 1 行和第 2 行,由于這兩行代碼不是聲明操作,所以 JavaScript 引擎不會做任何處理;

  • 第 3 行,由于這行是經(jīng)過 var 聲明的,因此 JavaScript 引擎將在環(huán)境對象中創(chuàng)建一個名為 myname 的屬性,并使用 undefined 對其初始化;

  • 第 4 行,JavaScript 引擎發(fā)現(xiàn)了一個通過 function 定義的函數(shù),所以它將函數(shù)定義存儲到堆 (HEAP)中,并在環(huán)境對象中創(chuàng)建一個 showName 的屬性,然后將該屬性值指向堆中函數(shù)的位置(不了解堆也沒關(guān)系,JavaScript 的執(zhí)行堆和執(zhí)行棧我會在后續(xù)文章中介紹)。

這樣就生成了變量環(huán)境對象。接下來 JavaScript 引擎會把聲明以外的代碼編譯為字節(jié)碼,至于字節(jié)碼的細(xì)節(jié),我也會在后面文章中做詳細(xì)介紹,你可以類比如下的模擬代碼:

showName()
console.log(myname)
myname = '極客時間'

好了,現(xiàn)在有了執(zhí)行上下文和可執(zhí)行代碼了,那么接下來就到了執(zhí)行階段了。

2. 執(zhí)行階段

JavaScript 引擎開始執(zhí)行“可執(zhí)行代碼”,按照順序一行一行地執(zhí)行。下面我們就來一行一行分析下這個執(zhí)行過程:

  • 當(dāng)執(zhí)行到 showName 函數(shù)時,JavaScript 引擎便開始在變量環(huán)境對象中查找該函數(shù),由于變量環(huán)境對象中存在該函數(shù)的引用,所以 JavaScript 引擎便開始執(zhí)行該函數(shù),并輸出“函數(shù) showName 被執(zhí)行”結(jié)果。

  • 接下來打印“myname”信息,JavaScript 引擎繼續(xù)在變量環(huán)境對象中查找該對象,由于變量環(huán)境存在 myname 變量,并且其值為 undefined,所以這時候就輸出 undefined。

  • 接下來執(zhí)行第 3 行,把“極客時間”賦給 myname 變量,賦值后變量環(huán)境中的 myname 屬性值改變?yōu)椤皹O客時間”,變量環(huán)境如下所示:

VariableEnvironment:
     myname -> "極客時間", 
     showName ->function : {console.log(myname)

好了,以上就是一段代碼的編譯和執(zhí)行流程 。

代碼中出現(xiàn)相同的變量或者函數(shù)怎么辦?

現(xiàn)在你已經(jīng)知道了,在執(zhí)行一段 JavaScript 代碼之前,會編譯代碼,并將代碼中的函數(shù)和變量保存到執(zhí)行上下文的變量環(huán)境中,那么如果代碼中出現(xiàn)了重名的函數(shù)或者變量,JavaScript 引擎會如何處理?

我們先看下面這樣一段代碼:

function showName() {
    console.log('極客邦');
}
showName();
function showName() {
    console.log('極客時間');
}
showName();

在上面代碼中,我們先定義了一個 showName 的函數(shù),該函數(shù)打印出來“極客邦”;然后調(diào)用 showName,并定義了一個 showName 函數(shù),這個 showName 函數(shù)打印出來的是“極客時間”;最后接著繼續(xù)調(diào)用 showName。那么你能分析出來這兩次調(diào)用打印出來的值是什么嗎?

我們來分析下其完整執(zhí)行流程:

  • 首先是編譯階段。遇到了第一個 showName 函數(shù),會將該函數(shù)體存放到變量環(huán)境中。接下來是第二個 showName 函數(shù),繼續(xù)存放至變量環(huán)境中,但是變量環(huán)境中已經(jīng)存在一個 showName 函數(shù)了,此時,第二個 showName 函數(shù)會將第一個 showName 函數(shù)覆蓋掉。這樣變量環(huán)境中就只存在第二個 showName 函數(shù)了。

  • 接下來是執(zhí)行階段。先執(zhí)行第一個 showName 函數(shù),但由于是從變量環(huán)境中查找 showName 函數(shù),而變量環(huán)境中只保存了第二個 showName 函數(shù),所以最終調(diào)用的是第二個函數(shù),打印的內(nèi)容是“極客時間”。第二次執(zhí)行 showName 函數(shù)也是走同樣的流程,所以輸出的結(jié)果也是“極客時間”。

綜上所述,一段代碼如果定義了兩個相同名字的函數(shù),那么最終生效的是最后一個函數(shù)。

關(guān)于“javascript變量提升的相關(guān)知識有哪些”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學(xué)到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI