JavaScript的執行機制——變數提升(範例詳解)

2022-02-23 19:00:49
本篇文章給大家帶來了關於變數提升的相關知識,什麼是變數提升,為什麼會有變數提升,下面一起來看一下吧,希望對大家有幫助。

相關推薦:

咱們先看段程式碼,你覺得下面這段程式碼輸出的結果是什麼?

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

使用過 JavaScript 開發的程式設計師應該都知道,JavaScript 是按順序執行的。若按照這個邏輯來理解的話,那麼:

  • 當執行到第 1 行的時候,由於函數 showName 還沒有定義,所以執行應該會報錯;
  • 同樣執行第 2 行的時候,由於變數 myname 也未定義,所以同樣也會報錯。

然而實際執行結果卻並非如此, 如下圖:
在這裡插入圖片描述
第 1 行輸出「函數 showName 被執行」,第 2 行輸出「undefined」,這和前面想象中的順序執行有點不一樣啊!

通過上面的執行結果,你應該已經知道了函數或者變數可以在定義之前使用,那如果使用沒有定義的變數或者函數,JavaScript 程式碼還能繼續執行嗎?為了驗證這點,我們可以刪除第 3 行變數 myname 的定義,如下所示:

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

然後再次執行這段程式碼時,JavaScript 引擎就會報錯,結果如下:
在這裡插入圖片描述
從上面兩段程式碼的執行結果來看,我們可以得出如下三個結論:

  • 在執行過程中,若使用了未宣告的變數,那麼 JavaScript 執行會報錯。
  • 在一個變數定義之前使用它,不會出錯,但是該變數的值會為 undefined,而不是定義時的值。
  • 在一個函數定義之前使用它,不會出錯,且函數能正確執行。

第一個結論很好理解,因為變數沒有定義,這樣在執行 JavaScript 程式碼時,就找不到該變數,所以 JavaScript 會丟擲錯誤。

但是對於第二個和第三個結論,就挺讓人費解的:

  • 變數和函數為什麼能在其定義之前使用?這似乎表明 JavaScript 程式碼並不是一行一行執行的。
  • 同樣的方式,變數和函數的處理結果為什麼不一樣?比如上面的執行結果,提前使用的 showName 函數能列印出來完整結果,但是提前使用的 myname 變數值卻是 undefined,而不是定義時使用的「極客時間」這個值。

變數提升(Hoisting)

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

不過在介紹變數提升之前,我們先通過下面這段程式碼,來看看什麼是 JavaScript 中的宣告和賦值。

var myname = '極客時間'

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

var myname    //宣告部分
myname = '極客時間'  //賦值部分

如下圖所示:
在這裡插入圖片描述
上面是變數的宣告和賦值,那接下來我們再來看看函數的宣告和賦值,結合下面這段程式碼:

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

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

第一個函數 foo 是一個完整的函數宣告,也就是說沒有涉及到賦值操作;第二個函數是先宣告變數 bar,再把function(){console.log(‘bar’)}賦值給 bar。為了直觀理解,你可以參考下圖:
在這裡插入圖片描述
好了,理解了宣告和賦值操作,那接下來我們就可以聊聊什麼是變數提升了。

所謂的變數提升,是指在 JavaScript 程式碼執行過程中,JavaScript 引擎把變數的宣告部分和函數的宣告部分提升到程式碼開頭的「行為」。變數被提升後,會給變數設定預設值,這個預設值就是我們熟悉的 undefined。

下面我們來模擬下實現:

/*
* 變數提升部分
*/// 把變數 myname提升到開頭,// 同時給myname賦值為undefinedvar myname = undefined// 把函數showName提升到開頭function showName() {
    console.log('showName被呼叫');}/*
 * 可執行程式碼部分
*/showName()console.log(myname)// 去掉var宣告部分,保留賦值語句myname = '極客時間'

為了模擬變數提升的效果,我們對程式碼做了以下調整,如下圖:
在這裡插入圖片描述
從圖中可以看出,對原來的程式碼主要做了兩處調整:

  • 第一處是把宣告的部分都提升到了程式碼開頭,如變數 myname 和函數 showName,並給變數設定預設值 undefined;
  • 第二處是移除原本宣告的變數和函數,如var myname = '極客時間’的語句,移除了 var 宣告,整個移除 showName 的函數宣告。

通過這兩步,就可以實現變數提升的效果。你也可以執行這段模擬變數提升的程式碼,其輸出結果和第一段程式碼應該是完全一樣的。

通過這段模擬的變數提升程式碼,相信你已經明白了可以在定義之前使用變數或者函數的原因——函數和變數在執行之前都提升到了程式碼開頭

JavaScript 程式碼的執行流程

從概念的字面意義上來看,「變數提升」意味著變數和函數的宣告會在物理層面移動到程式碼的最前面,正如我們所模擬的那樣。但,這並不準確。實際上變數和函數宣告在程式碼裡的位置是不會改變的,而且是在編譯階段被 JavaScript 引擎放入記憶體中。對,你沒聽錯,一段 JavaScript 程式碼在執行之前需要被 JavaScript 引擎編譯,編譯完成之後,才會進入執行階段。大致流程你可以參考下圖:
在這裡插入圖片描述

1. 編譯階段

那麼編譯階段和變數提升存在什麼關係呢?

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

第一部分:變數提升部分的程式碼。

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

第二部分:執行部分的程式碼。

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

下面我們就可以把 JavaScript 的執行流程細化,如下圖所示:

在這裡插入圖片描述
從上圖可以看出,輸入一段程式碼,經過編譯後,會生成兩部分內容:執行上下文(Execution context)和可執行程式碼。

執行上下文是 JavaScript 執行一段程式碼時的執行環境,比如呼叫一個函數,就會進入這個函數的執行上下文,確定該函數在執行期間用到的諸如 this、變數、物件以及函數等。

關於執行上下文的細節,我會在下一篇文章《08 | 呼叫棧:為什麼 JavaScript 程式碼會出現棧溢位?》做詳細介紹,現在你只需要知道,在執行上下文中存在一個變數環境的物件(Viriable Environment),該物件中儲存了變數提升的內容,比如上面程式碼中的變數 myname 和函數 showName,都儲存在該物件中。

你可以簡單地把變數環境物件看成是如下結構:

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

瞭解完變數環境物件的結構後,接下來,我們再結合下面這段程式碼來分析下是如何生成變數環境物件的。

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

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

  • 第 1 行和第 2 行,由於這兩行程式碼不是宣告操作,所以 JavaScript 引擎不會做任何處理;
  • 第 3 行,由於這行是經過 var 宣告的,因此 JavaScript 引擎將在環境物件中建立一個名為 myname 的屬性,並使用 undefined 對其初始化;
  • 第 4 行,JavaScript 引擎發現了一個通過 function 定義的函數,所以它將函數定義儲存到堆 (HEAP)中,並在環境物件中建立一個 showName 的屬性,然後將該屬性值指向堆中函數的位置(不瞭解堆也沒關係,JavaScript 的執行堆和執行棧我會在後續文章中介紹)。

這樣就生成了變數環境物件。接下來 JavaScript 引擎會把宣告以外的程式碼編譯為位元組碼,至於位元組碼的細節,我也會在後面文章中做詳細介紹,你可以類比如下的模擬程式碼:

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

好了,現在有了執行上下文和可執行程式碼了,那麼接下來就到了執行階段了。

2. 執行階段

JavaScript 引擎開始執行「可執行程式碼」,按照順序一行一行地執行。下面我們就來一行一行分析下這個執行過程:

  • 當執行到 showName 函數時,JavaScript 引擎便開始在變數環境物件中查詢該函數,由於變數環境物件中存在該函數的參照,所以 JavaScript 引擎便開始執行該函數,並輸出「函數 showName 被執行」結果。
  • 接下來列印「myname」資訊,JavaScript 引擎繼續在變數環境物件中查詢該物件,由於變數環境存在 myname 變數,並且其值為 undefined,所以這時候就輸出 undefined。
  • 接下來執行第 3 行,把「極客時間」賦給 myname 變數,賦值後變數環境中的 myname 屬性值改變為「極客時間」,變數環境如下所示:
VariableEnvironment:
     myname -> "極客時間", 
     showName ->function : {console.log(myname)

好了,以上就是一段程式碼的編譯和執行流程 。

程式碼中出現相同的變數或者函數怎麼辦?

現在你已經知道了,在執行一段 JavaScript 程式碼之前,會編譯程式碼,並將程式碼中的函數和變數儲存到執行上下文的變數環境中,那麼如果程式碼中出現了重名的函數或者變數,JavaScript 引擎會如何處理?

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

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

在上面程式碼中,我們先定義了一個 showName 的函數,該函數列印出來「極客邦」;然後呼叫 showName,並定義了一個 showName 函數,這個 showName 函數列印出來的是「極客時間」;最後接著繼續呼叫 showName。那麼你能分析出來這兩次呼叫列印出來的值是什麼嗎?

我們來分析下其完整執行流程:

  • 首先是編譯階段。遇到了第一個 showName 函數,會將該函數體存放到變數環境中。接下來是第二個 showName 函數,繼續存放至變數環境中,但是變數環境中已經存在一個 showName 函數了,此時,第二個 showName 函數會將第一個 showName 函數覆蓋掉。這樣變數環境中就只存在第二個 showName 函數了。
  • 接下來是執行階段。先執行第一個 showName 函數,但由於是從變數環境中查詢 showName 函數,而變數環境中只儲存了第二個 showName 函數,所以最終呼叫的是第二個函數,列印的內容是「極客時間」。第二次執行 showName 函數也是走同樣的流程,所以輸出的結果也是「極客時間」。

綜上所述,一段程式碼如果定義了兩個相同名字的函數,那麼最終生效的是最後一個函數。

總結

好了,今天就到這裡,下面我來簡單總結下今天的主要內容:

  • JavaScript 程式碼執行過程中,需要先做變數提升,而之所以需要實現變數提升,是因為 JavaScript 程式碼在執行之前需要先編譯。
  • 在編譯階段,變數和函數會被存放到變數環境中,變數的預設值會被設定為 undefined;在程式碼執行階段,JavaScript 引擎會從變數環境中去查詢自定義的變數和函數。
  • 如果在編譯階段,存在兩個相同的函數,那麼最終存放在變數環境中的是最後定義的那個,這是因為後定義的會覆蓋掉之前定義的。

相關推薦:

以上就是JavaScript的執行機制——變數提升(範例詳解)的詳細內容,更多請關注TW511.COM其它相關文章!