進入主題之前先來認識一個非常重要的觀念:Execution context 執行環境。
A wrapper to help manage the code that is running
雖然 JavaScript 是一個單執行緒、同步的程式,但在轉譯的過程中並不會逐行丟給電腦去執行。
我們的程式碼會在執行時,會先依照詞彙環境(Lexical Environment)被解析器轉換,在電腦中被創造並擺到該放的記憶體位置去,最後電腦才執行。尤其JS某些特性(例如這篇要講的hoisting)就是在創造階段產生,因此理解JS在執行環境中的狀態是很重要的一件事。
首先,讓我們看一段程式碼:
b();
console.log(a);
var a = "Hello World"
function b() {
console.log('Called b !')
}
我們會得到:
Called b!
undefined
為什麼印出來的會是undefined 而不是 error呢?
我們再試一個把宣告拿掉的:
b();
console.log(a);
// var a = 'Hello World!';
function b() {
console.log('Called b!');
}
我們會得到:
Called b!
ReferenceError: a is not defined
為什麼?????
第一次我以為會出現error結果沒有,第二次我以為會跟第一次一樣結果出現error。
這個現象就叫做 『hoisting』。
要了解JavaScript 之所以可以這麼做,我們要了解Execution context的創建方式:
其實Execution context 是分兩個階段創建的。變數跟函式在某總程度上是可以用,即便他們是寫在console 後面,就是因為這個原因。下面我們看看是分哪兩個階段:
第一階段是創造(creation)
在這個階段,JavaScript引擎會在儲存空間中確認要創建變數以及函式的位子,然後創建出來,但是還沒把值放到變數中,所以當程式碼開始執行時,我們才可以使用它們。
第二階段就是執行(execution)
執行時函式跟變數就有些不同了,在JavaScript裡,所有的變數在最初的設置都是undefined,而函式在最初設置就是完整的。
所以,我們可以這樣解讀:
JavaScript引擎在執行我們的程式碼,是先依照程式碼上我們宣告的變數跟函式在記憶體中創建他們的位子,然後才會開始一行一行執行下來,因為變數在最初的設置都是undefined,所以在執行到console.log(a)時,因為還沒有執行到 var a = “Hello World” 所以印出來的會是undefined。
PJ助教在自己的部落格有補充到:在我們定義變項的過程中,可以分成宣告(declaration)和給值(initialization)的兩個過程,只有declaration的內容會在逐行執行程式前先被執行並儲存在記憶體中(hoisted);給值的內容則是在hoisted後,逐行執行程式時,才會被執行到。
在剛剛的案例中,我們有看到兩個詞:undefined 跟 not defined。
undefined 跟 not defined 差在哪邊?
剛剛在說hoisting 時有提到,JavaScript 引擎會先在記憶體創建屬於變數的空間,並且在未賦值前都會是undefined。
在JavaScript中 undefined 是一個特殊關鍵值,代表尚未被定義。not defined 則是代表我們在儲存空間中沒有找到這個。
因此我們可以說 undefined是尚未給定義但已宣告變項的值,但是not defined則是該變項尚未宣告過,執行後會出現錯誤訊息!
我們來用兩段code來證明吧!
第一段:
console.log(a) // ReferenceError: a is not defined
let a
看起來好像沒有耶,如果有應該是出現 undefined
,但這邊卻是報錯。
那我們繼續看第二段:
var a = 10
function test(){
console.log(a)
let a
}
test()
如果真的沒有 hoisting 的話,答案應該會輸出10
。(應該不用解釋了吧XDD)
答案卻是:ReferenceError: a is not defined
。
所以我們可以說,其實let & const 是有 hoisting,但是跟var
的hoisting 有一點不一樣。
let & const 的 hoisting 與 var
的差別在於提升之後,var
宣告的變數會被初始化為 undefined,而 let 與 const 的宣告不會被初始化為 undefined,而且如果你在「賦值之前」就存取它,就會拋出錯誤。
在「提升之後」以及「賦值之前」這段「期間」,如果你存取它就會拋出錯誤,而這段期間就稱做是 TDZ,它是一個為了解釋 let 與 const 的 hoisting 行為所提出的一個名詞。
我們看下面的程式碼會更清楚:
function test() {
var a = 1; // c 的 TDZ 開始
var b = 2;
console.log(c) // 錯誤
if (a > 1) {
console.log(a)
}
let c = 10 // c 的 TDZ 結束
}
test()
當我們進入test這個function時,c 的 TDZ 就會開始,要一直等到let c = 10
c 的 TDZ 才會結束,只要在這段時間內你使用c就會發生錯誤。
用一句話來總結:
let 與 const 也有 hoisting 但沒有初始化為 undefined,而且在賦值之前試圖取值會發生錯誤。
雖然這篇只是短短的,但當時在”JavaScript: Understanding the Weird Parts“學習到這一段時真的有大開眼界的感覺,原來我之前根本沒有了解hoisting。
本篇大部份是照抄我的部落格文章,讓我水一天一下XD
https://blog.techbridge.cc/2018/11/10/javascript-hoisting/