提到前端面試,對於 javascript 語言層面的考察,這幾個概念是避不開的:執行上下文,變量提高,閉包,This,做用域,做用域鏈,原型鏈,Event Loop等。
與其說面試很機械,倒不如說這就是 javascript 語音最最核心的概念,弄不清楚這些概念,那你必定不是一名合格的前端開發er。
因此,接下來我會分幾篇文章來說這幾個核心概念,並將他們串起來,讓你們能夠更好的全方位理解。
下面進入正題,今天第一篇文章咱們來講 —— 變量提高。javascript
先看代碼:前端
showName() console.log(myname) var myname = 'wens' function showName() { console.log('函數showName被執行'); }
使用過 JavaScript 開發的程序員應該都知道,JavaScript 是按順序執行的。若按照這個邏輯來理解的話,那麼:java
第 1 行輸出「函數 showName 被執行」,第 2 行輸出「undefined」,這和想象中的順序執行有點不同啊!程序員
經過上面的執行結果,咱們已經知道了函數或者變量能夠在定義以前使用,那若是使用沒有定義的變量或者函數,JavaScript 代碼還能繼續執行嗎?爲了驗證這點,咱們能夠刪除第 3 行變量 myname 的定義,以下所示:面試
showName() console.log(myname) function showName() { console.log('函數showName被執行'); }
這時候 JavaScript 引擎就會報錯,結果以下:閉包
從上面兩段代碼的執行結果來看,咱們能夠得出以下三個結論。函數
第一個結論很好理解,由於變量沒有定義,這樣在執行 JavaScript 代碼時,就找不到該變量,因此 JavaScript 會拋出錯誤。oop
可是對於後兩個結論,就挺讓人費解的:優化
要解釋這兩個問題,咱們須要先了解下什麼是變量提高。this
不過在介紹變量提高以前,咱們先經過下面這段代碼,來看看什麼是 JavaScript 中的聲明和賦值。
var myname = 'wens'
這行代碼須要這樣理解:
var myname //聲明部分 myname = 'wens' //賦值部分
上面是變量的聲明和賦值,那接下來咱們再來結合代碼看看函數的聲明和賦值:
function foo(){ console.log('foo') } var bar = function(){ console.log('bar') }
第一個函數 foo 是一個完整的函數聲明,也就是說沒有涉及到賦值操做;
第二個函數和上面那一行變量的賦值同樣,是先聲明變量 bar,再把function(){console.log('bar')}賦值給 bar。
好了,理解了聲明和賦值操做,那接下來咱們就能夠聊聊什麼是變量提高了。
所謂的變量提高,是指在 JavaScript 代碼執行過程當中,JavaScript 引擎把變量的聲明部分和函數的聲明部分提高到代碼開頭的行爲。變量被提高後,會給變量設置默認值,這個默認值就是咱們熟悉的 undefined。
針對第一個代碼片斷,咱們來模擬一下變量提高:
// 把變量 myname提高到開頭, // 同時給myname賦值爲undefined var myname = undefined // 把函數showName提高到開頭 function showName() { console.log('showName被調用'); } showName() // 因此這裏能夠正常執行 console.log(myname) // 因此這裏能夠正常打印myname的值 // 去掉var聲明部分,保留賦值語句 myname = 'wens'
從代碼中能夠看出,對原來的代碼主要作了兩處調整:
經過這兩步,就能夠實現變量提高的效果。你也能夠執行這段模擬變量提高的代碼,其輸出結果和第一段代碼是徹底同樣的。
從字面意義上來看,「變量提高」意味着變量和函數的聲明會移動到代碼的最前面,就像咱們所模擬的那樣。其實這並不許確。實際上變量和函數聲明在代碼裏的位置是不會改變的。由於一段 JavaScript 的可執行代碼(executable code) 在真正被執行以前還要先經歷引擎的編譯
上面說到 JavaScript 的可執行代碼(executable code),那麼哪些是可執行代碼呢?
其實很簡單,就三種:
那麼編譯階段和變量提高存在什麼關係呢?
爲了搞清楚這個問題,咱們仍是回過頭來看上面那段模擬變量提高的代碼,爲了方便介紹,能夠把這段代碼分紅兩部分。
從上圖能夠看出,輸入一段代碼,通過編譯後,會生成兩部份內容:執行上下文(Execution context)和可執行代碼。
執行上下文是 JavaScript 執行一段代碼時的運行環境,好比調用一個函數,就會進入這個函數的執行上下文,在這裏肯定該函數在執行期間用到的諸如 this、變量、對象以及函數等。
關於執行上下文的細節,我會在下一篇文章作詳細介紹,如今咱們只須要知道,在執行上下文中存在一個變量環境的對象(Viriable Environment),該對象中保存了變量提高的內容,好比上面代碼中的變量 myname 和函數 showName,都保存在該對象中。
咱們能夠簡單地把變量環境對象當作是以下結構:
VariableEnvironment: myname -> undefined, showName -> function {console.log(myname)}
接下來,咱們再結合代碼來分析下是如何生成變量環境對象的:
showName() console.log(myname) var myname = 'wens' function showName() { console.log('函數showName被執行'); }
咱們逐行分析上述代碼:
這樣就生成了變量環境對象。接下來 JavaScript 引擎會把聲明之外的代碼編譯爲字節碼,至於字節碼的細節,我也會在後面文章中作詳細介紹。如今有了執行上下文和可執行代碼了,那麼接下來就到了執行階段了。
JavaScript 引擎開始執行「可執行代碼」,按照順序逐行執行。下面咱們就分析下這個執行過程:
VariableEnvironment: myname -> wens, showName -> function {console.log(myname)}
好了,以上就是一段代碼的編譯和執行流程。實際上,編譯階段和執行階段都是很是複雜的,包括了詞法分析、語法解析、代碼優化、代碼生成等,全部的這些內容我都會在接下來的文章中介紹,在本篇文章中咱們暫時只介紹變量提高相關內容。
如今咱們知道了,在執行一段 JavaScript 代碼以前,會編譯代碼,並將代碼中的函數和變量保存到執行上下文的變量環境中,那麼若是代碼中出現了重名的函數或者變量,JavaScript 引擎會如何處理?
咱們看下面的代碼:
console.log(myName); var myName = 'wens'; console.log(myName); var myName = 'leon'; console.log(myName);
咱們來分析下其完整執行流程:
綜上所述,一段代碼若是定義了兩個相同名字的值,那麼最終的值取決於被賦予的最新的值,若是沒有就是初始值undefined。
咱們先看下面這樣一段代碼:
function showName() { console.log('wens'); } showName(); function showName() { console.log('leon'); } showName();
在上面代碼中,咱們先定義了一個 showName 的函數,該函數打印出來「wens」;而後調用 showName,又定義了一個 showName 函數,這個 showName 函數打印出來的是「leon」;一段代碼中出現了同名的函數,並分別調用。
咱們來分析下其完整執行流程:
綜上所述,一段代碼若是定義了兩個相同名字的函數,那麼最終生效的是最後一個函數。
好了,今天就到這裏,下面我來簡單總結下今天的主要內容:
以上就是今天所講的主要內容,接下來的文章咱們會重點介紹執行上下文。