JavaScript的hoisting這個特性在初學時期可能會偶爾不小心碰到,但不太會有什麼深刻的感受,或是不知道原來這就是hoisting。但是近期參與重構主題的讀書會,有討論到一些寫法和hoisting這個特性有關,才發現其實這個特性其實真的很需要好好認識,加上前陣子因為一些面談的過程,發現自己其實對這個概念有些錯誤的理解,所以就想說來好好地再幫自己釐清和整理一些這部分的必懂常識囉!
什麼是hoisting
首先,不免俗地要來說文解字一下!直接看字面的話,沒有什麼花俏的說明,hoisting的字面意思就是「提升」。再深入一點理解,就會是「提升」某些東西,某些東西被「提升」了。看起來好像很廢話,總之就是「提升」。
進到MDN看一下更詳細的說明吧!
從上述這段說明可以看到關於「提升」的關鍵句子,也就是"move the declaration of functions, variables or classes to the top of their scope",大致上的意思就是是「函式、變數的宣告會被提升到作用域頂部」。
如何產生hoisting
雖然前面的那句話已經解釋了何謂hoisting,但是老實說如果還沒有深刻透過實作來理解的話,可能還是很難單單透過這句的說明就掌握到什麼是hoisting。在進一步看宣告會被hoisting的對象之前,先來從另一個角度了解hoisting是如何產生的吧!
這部分要回到最根本去理解JavaScript建立執行環境的過程,JavaScript在建立執行環境(Execution context)時,主要會分為兩個階段:創建階段(Creation Phase)、執行階段(Execute Phase)
第一個階段、創造階段
在這個階段主要會做以下幾件事情:
- 在記憶體中幫變數、函式建立一個空間,這就是和這次提到的hoisting有關的部分。
- 將全域物件(Global Object)、this設定到記憶體中
- 建立外部環境(Outer Environment)
第二階段、執行階段
在這個階段會逐行執行程式碼,並編譯成電腦懂的內容,如果在執行程式碼的時候,找不到當下程式碼中要使用的變數,就會往一層一層往外找。
來段程式碼來看看上面所說的階段!
如果以大部分的人閱讀文字的習慣來看上面這段程式碼的話,可能會覺得以上程式碼執行後的狀況應該會印出以下的內容:
呼叫函式
ReferenceError: variable is not defined
我是變數
但是實際執行過後,卻可以發現到並不是以上的內容,而是這樣
這就是我們前面提到的分為兩個階段建立執行環境,所產生的結果。
再來回頭看一下前面的那段程式碼,搭配建立執行環境的兩個階段下去思考的話,實際上在執行這段程式碼時,會經歷的步驟會是以下這樣:
1. 快速掃過一遍程式碼,並替掃到的變數和函式建立記憶體空間,var變數的部分預設值會是undefined。
2. 逐行「執行」程式碼,變數還沒被賦值前,其值會是創建階段給的undefined,賦值後才會變成賦值後的字串。
用白話一點來說的話,JavaScript在建立執行環境時,會先掃過一次程式看現在有哪些變數和函式被宣告,並替它們創建記憶體的空間,也就是說這個時候這些被宣告的變數和記憶體都已經是存在著的東西了,且如果是變數的話,它預設值會是undefined。再來才會真正進入逐行執行程式碼的階段,所以程式碼文字上看得到的賦值動作是在這個階段才執行。但由於第一階段已經有讓宣告的function和變數存在在記憶體中,所以即使書寫順序不是先宣告再呼叫,而是倒過來地先寫呼叫的程式碼,再寫宣告的程式碼,還是可以行得通。
以這個概念下去想的話,感覺就像是這些被宣告的變數會函式被提升到作用域最頂端了,但實際上肉眼看到的程式碼,並不是放在最頂端,這也就是所謂的"hoisting"。
hoisting的對象
接下來再看看是誰被宣告時會hoisting?前面那個關鍵句子其實已經有提到會被hoisting的對象了,也就是函式和變數。
知道對象是誰後,再來實際看一下hoisting的效果吧!
- 函式(function)
如果以程式碼撰寫順序來看,應該會覺得這樣寫會噴錯,但可以再回頭思考一下前面提到的hoisting,這樣的寫法其實是可以正常呼叫function的喔!
因為在建立執行環境的第一個階段就把宣告的函式記憶體空間建立了,所以在還沒正式呼叫函式前,其實函式就會在,雖然肉眼看到的程式碼排序是函式的宣告在呼叫函式的內容後面。
執行結果:
- 變數(variable)
變數又分成以下幾種宣告方式,也個別看一下會出現什麼樣的情況。
1. var
變數也跟函式一樣,因為在創建執行環境的第一個階段時,就已經有建立這個變數的記憶體空間,並且先給了一個undefined的預設值,所以可以印出undefined,再第二階段中賦值過後再印變數,才會印出「我是變數」的字串。
執行結果:
2. let & const
好了!有了前面用var變數宣告的例子了,let和const變數應該是一樣的吧?直接看看執行結果吧。
執行結果:
結果不管是用let還是const宣告,執行程式出現的結果都是這上面這個樣子。
看到這個結果的人心中應該會有個os「蝦米!!!是在跟我開什麼玩笑!前面說的概念怎麼跟這裡對不上呢?一樣是宣告變數啊!怎麼感覺沒有hoisting了?怎麼會噴錯?」
這也是我原本對於hoisting理解錯誤的部分ㅠㅠ!
雖然用let或const宣告有跳錯,但它還是有hoisting。
口說無憑,來證明一下。
上一段驗證的程式碼:
在這種情況下,如果沒有hoisting的話,照理說會往函式外找a這個變數,所以如果沒有hoisting的話,應該會印出'我是外面的a'字串才對,但是實際執行後的結果卻是以下這樣。
所以也就代表let或const宣告的變數還是會hoisting,只是呈現的結果有點差異,而造成這個差異的原因就是var和let、const的暫時死區(TDZ)結束的時間點不同。
暫時死區 (TDZ)
TDZ是temporal dead zone的縮寫,中文翻譯成「暫時死區」。
當變數還沒被進行初始化,給定一個初始值,還無法被存取時,就會進入暫時死區。前面有提到當創建執行環境時,如果是變數的話,在第一個階段會被建立記憶體的空間,在當下還會給一個初始值,但是這只會出現在用var宣告變數的時候,也就是說用var宣告的時候,在創建階段就會被賦值,所以當下就會結束暫時死區。不過如果是用let或const宣告變數,並不會在當下就初始化並給定任一個值當作初始值,在真正被賦值之前,這個變數就會一直處在暫時死區,導至在真正賦值之前,如果要存去這個變數,就會出現"Cannot access 'a' before initializatiion"的錯誤。換個說法來說的話,就是let和const的暫時死區會比var還晚結束。
hoisting的優先權
hoisting還有一個很有趣的部分,那就是變數和函式間的優先權有差異。先從結論說的話,函式hoisting的優先權會高於變數。
直接來看看程式碼!
當宣告同名字的變數和函式,會發生什麼樣的事情呢?
就如同前面提到的結論一樣,由於函式的hoisting優先權高於變數,所以執行上方的程式碼會印出function。
總結
最後來回顧幾個前面提到過的重點。
1. hoisting會出現在宣告的動作。
2 hoisting的特性與JavaScript建立執行環境分為兩個階段有關,這兩個階段可以簡單地理解成先宣告再執行,而讓程式碼從肉眼上並沒有移動位置,但實際執行結果卻又像是宣告的程式碼都被寫在最上方。
3. 不只var會hoisting,let、const的宣告動作也會hoisting,差異只在於用let、const的暫時死區(TDZ)結束的時間比較晚,所以會跳出錯誤,而不是印出undefined。
4. hoisting有優先權的差異,函式高於變數。
這次從重新好好理解hoisting後,對於hoisting的理解又更加提升了,而且還進一步去了解什麼是TDZ,這部分也還滿有趣的。另外,最近也在思考在寫整體程式碼的風格上,應該也要善用hoisting的特性,讓整體的程式碼更好閱讀。總之,最近真的很常碰到hoisting,也有好好趁這個機會重新學習了一下,覺得收穫不少,也就特別給它記錄下來了!
好了!到這裡也差不多打完收工了!
那就寫些打家!打家掰掰!
留言列表