相關推薦:
咱們先看段程式碼,你覺得下面這段程式碼輸出的結果是什麼?
showName() console.log(myname) var myname = '極客時間' function showName() { console.log('函數showName被執行'); }
使用過 JavaScript 開發的程式設計師應該都知道,JavaScript 是按順序執行的。若按照這個邏輯來理解的話,那麼:
然而實際執行結果卻並非如此, 如下圖:
第 1 行輸出「函數 showName 被執行」,第 2 行輸出「undefined」,這和前面想象中的順序執行有點不一樣啊!
通過上面的執行結果,你應該已經知道了函數或者變數可以在定義之前使用,那如果使用沒有定義的變數或者函數,JavaScript 程式碼還能繼續執行嗎?為了驗證這點,我們可以刪除第 3 行變數 myname 的定義,如下所示:
showName() console.log(myname) function showName() { console.log('函數showName被執行'); }
然後再次執行這段程式碼時,JavaScript 引擎就會報錯,結果如下:
從上面兩段程式碼的執行結果來看,我們可以得出如下三個結論:
第一個結論很好理解,因為變數沒有定義,這樣在執行 JavaScript 程式碼時,就找不到該變數,所以 JavaScript 會丟擲錯誤。
但是對於第二個和第三個結論,就挺讓人費解的:
要解釋這兩個問題,你就需要先了解下什麼是變數提升。
不過在介紹變數提升之前,我們先通過下面這段程式碼,來看看什麼是 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 = '極客時間'
為了模擬變數提升的效果,我們對程式碼做了以下調整,如下圖:
從圖中可以看出,對原來的程式碼主要做了兩處調整:
通過這兩步,就可以實現變數提升的效果。你也可以執行這段模擬變數提升的程式碼,其輸出結果和第一段程式碼應該是完全一樣的。
通過這段模擬的變數提升程式碼,相信你已經明白了可以在定義之前使用變數或者函數的原因——函數和變數在執行之前都提升到了程式碼開頭。
從概念的字面意義上來看,「變數提升」意味著變數和函數的宣告會在物理層面移動到程式碼的最前面,正如我們所模擬的那樣。但,這並不準確。實際上變數和函數宣告在程式碼裡的位置是不會改變的,而且是在編譯階段被 JavaScript 引擎放入記憶體中。對,你沒聽錯,一段 JavaScript 程式碼在執行之前需要被 JavaScript 引擎編譯,編譯完成之後,才會進入執行階段。大致流程你可以參考下圖:
那麼編譯階段和變數提升存在什麼關係呢?
為了搞清楚這個問題,我們還是回過頭來看上面那段模擬變數提升的程式碼,為了方便介紹,可以把這段程式碼分成兩部分。
第一部分:變數提升部分的程式碼。
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被執行'); }
我們可以一行一行來分析上述程式碼:
這樣就生成了變數環境物件。接下來 JavaScript 引擎會把宣告以外的程式碼編譯為位元組碼,至於位元組碼的細節,我也會在後面文章中做詳細介紹,你可以類比如下的模擬程式碼:
showName() console.log(myname) myname = '極客時間'
好了,現在有了執行上下文和可執行程式碼了,那麼接下來就到了執行階段了。
JavaScript 引擎開始執行「可執行程式碼」,按照順序一行一行地執行。下面我們就來一行一行分析下這個執行過程:
VariableEnvironment: myname -> "極客時間", showName ->function : {console.log(myname)
好了,以上就是一段程式碼的編譯和執行流程 。
現在你已經知道了,在執行一段 JavaScript 程式碼之前,會編譯程式碼,並將程式碼中的函數和變數儲存到執行上下文的變數環境中,那麼如果程式碼中出現了重名的函數或者變數,JavaScript 引擎會如何處理?
我們先看下面這樣一段程式碼:
function showName() { console.log('極客邦'); } showName(); function showName() { console.log('極客時間'); } showName();
在上面程式碼中,我們先定義了一個 showName 的函數,該函數列印出來「極客邦」;然後呼叫 showName,並定義了一個 showName 函數,這個 showName 函數列印出來的是「極客時間」;最後接著繼續呼叫 showName。那麼你能分析出來這兩次呼叫列印出來的值是什麼嗎?
我們來分析下其完整執行流程:
綜上所述,一段程式碼如果定義了兩個相同名字的函數,那麼最終生效的是最後一個函數。
好了,今天就到這裡,下面我來簡單總結下今天的主要內容:
相關推薦:
以上就是JavaScript的執行機制——變數提升(範例詳解)的詳細內容,更多請關注TW511.COM其它相關文章!