JavaScript 修練 (1) | 變數的宣告、作用域與提升
沒有宣告變數會怎樣?
宣告變數的目的在於賦予變數一個初始值,並限制它的作用域。
在學習 JavaScript 的過程中,總是會一直被重複提醒變數需要被宣告,但是一開始學習 JavaScript 並不會了解為什麼要這樣做,這背後的原因到底是什麼
這篇文章將介紹有關變數宣告、作用域及提升相關的內容,接下來就用幾個範例,透過比較來了解到底為什麼吧 ~
沒有宣告的變數會變成全域「屬性」
相信在學習過程中常常會聽到有人說:「沒有宣告的變數會變成全域『變數』」,這句話其實嚴格來說不太正確,正確來說是:「沒有宣告的變數會變成全域『屬性』」
先來比較有宣告的變數 a 及沒有宣告的變數 b,到底有什麼差異
1 | var a = 1; |
展開執行結果會發現全域物件也就是 window 裡面,出現了 a 跟 b 兩個屬性
這時候你就會感覺到混亂,所以 a 跟 b 誰是屬性?誰是變數?
先別急,接著往下走,如果你有一些物件的基礎知識,應該會知道物件內有屬性這個東西,而 window 是一個全域的物件,所以 window 內也會有它的屬性,而沒有宣告的所謂「變數 b」,其實在 JavaScript 眼中是這樣的,前面省略了 window
1 | window.b = 2; |
所以你以為你在幫變數賦予值,但其實是在幫 window 這個全域物件新增一個屬性 b 並賦予它值為 2,所以 b 其實是全域的屬性不是變數
可是用 var 宣告的變數 a 呢?它也在 window 裡面,怎麼證明他們不一樣?
接下來就派出 delete 這個運算子了,delete 運算子可以用來刪除物件中的屬性,而變數是無法被刪除的,那把它拿來刪除 a 跟 b 會發生什麼事呢?
1 | var a = 1; |
執行完你會發現 window 裡面的 b 被成功刪除了,但是 a 卻沒有
所以 b 其實是全域 window 屬性,可以使用 delete 刪除它;而 a 在全域環境使用 var 宣告,則為全域變數,無法使用 delete 刪除它
未宣告的變數其實是全域 window 下的一個屬性不是真的變數,而全域的 window 沒有作用域限制,在哪都可以被存取
在全域環境下用 var 宣告的變數,雖然是變數但也會在 window 下建立屬性,它並不會被 delete 刪除,所以無論用 window.a 或是用 a 都能取得變數的值
小補充:let、const 宣告的全域變數
這邊補充在全域環境使用 let 或 const 宣告的全域變數,不會在 window 下建立屬性
1 | var a = 1; |
我就不宣告怎麼樣
看了以上這個多解釋,可能還是不太理解到底不宣告變數的後果是什麼,那就讓我們叛逆一次示範一下 (不聽話的下場) 會發生什麼事
前面有提到未宣告會變成全域屬性,而全域的 window 沒有作用域限制,所以在每個函式和作用域內都可以取得 window 及 window 下的所有屬性
如果你跟我一樣 剛好是金魚腦,在寫了幾百行程式碼之後可能會發生以下慘劇
1 | function fn() { |
以上程式碼中 family 在沒有宣告的情況下,它變成了全域屬性,開發時在任何函式中都可以輕鬆呼叫 family,而開發過程中寫了幾百行程式碼後,有可能會忘記定義了哪些全域屬性,所以又用了同樣的名稱來進行開發,結果就是小美家直接變成空物件,程式碼無法運作但你找不到原因
一個人開發都有可能會因為忘記而出錯,更不要說跟團隊一起開發,未宣告的變數會變成全域屬性,造成全域環境的作用域汙染,導致其他命名覆蓋問題,這可能會導致除錯上的困難
變數的作用域
前面提到未宣告變數的後果時,有說到作用域這些詞,接下來就來說說變數的「作用域」( Scope ) 吧 ~
JavaScript 有一個特別的機制稱為「語法作用域」( Lexical scope ),又稱為「靜態作用域」,意思是原始碼經過直譯器執行後就已經確定了作用域且不會再改變,這讓開發者可以直接透過原始碼了解當前的變數作用域
簡單來說所謂的語法作用域就是你宣告時就已經決定好他的作用域,而變數宣告後會被限制其作用域,作用域會因為宣告語法不同 ( var、let、const ) 而有變化
全域變數與區域變數
如果舉例來形容全域變數與區域變數,全域變數就像全國的補助、而區域變數則像縣市補助
- 全域變數 :全國補助,全國各地都能申請
- 區域變數 :縣市補助,只有在特定縣市內才能申請
兩者作用域範圍不同,全域代表的是最外層,內層可能是各個函式或作用域,而 在內層可以存取在外層宣告的變數,在外層則無法存取在內層宣告的變數
另外,假設在內層沒有宣告過 b 變數,則會透過「向外查找」的機制,一層一層向外尋找是否有叫 b 的變數,直到最外層全域,如果連全域都沒有 b 變數才會跳出「b is not defined」的錯誤訊息
接下來看幾個範例
在外層無法存取在 fn 函式內宣告的區域變數 b
1 | // 全域變數 |
在內層 fn2 函式內可以存取在 fn 函式內宣告的區域變數 b
1 | // 全域變數 |
var 屬於函式作用域
前面一直用 var 在宣告變數,接下來就來說說 var 宣告的變數作用域範圍
var 的作用域範圍在該函式內 ,在該函式內宣告的變數會被限制在該函式作用域中,也就是該變數只能在該函式中取得,有點像是把變數關在叫做函式的小房間中,變數的活動範圍就被限制了
以下範例在 fn 函式內被宣告的變數 b,無法在 fn2 函式內被存取
1 | var a = '小明'; |
let、const 屬於區塊作用域
隨著 ES6 成為主流,現在都會建議使用 let、const 來宣告變數,其中 let 宣告的變數可以重新賦值、const 則無法重新賦值
前面有提到 var 屬於「函式作用域」( function scope ),而 let、const 的作用域則屬於「區塊作用域」( block scope )
區塊其實就是大括號 {}
,前面有比喻函式作用域像是把變數關進叫做函式的小房間,那麼這裡的區塊作用域就像是把變數關進大括號裡面,限制它的活動範圍 ( 變數:結果還是把我關起來了QAQ
除了前面函式的 {}
大括號可以被認定為是一個區塊之外,還有其他在實戰中常見的區塊作用域:
- 函式 :
function fn() {...}
- for 迴圈 :
for(let i=0; i<10; i++) {...}
- 判斷式 :
if() {...}
- 純粹的區塊 :
{}
其中用常見的 for 迴圈來比較 var 和 let 宣告變數的作用域差異
1 | for(var i = 0; i < 10; i++){} |
這邊可以看見 var 因為屬於函式作用域,顯然 {}
困不住裡面的變數,所以當在 for 迴圈的小括號中用 var 宣告變數時,這個變數會成為全域變數,或是其他函式內的區域變數 ( 如果迴圈在其他函式內的話 )
在上面的範例,變數 i 會變成全域變數,在其他地方也能被存取修改,容易汙染作用域環境,而用屬於區塊作用域的 let 宣告,變數的作用域不需要用函式,只要有 {}
就能被限制住,避免產生全域變數,所以使用 let、const 宣告的變數,相對於 var 來說更能避免產生不必要的問題
提升
「提升」( Hoisting ) 一詞並未在 ECMAScript 2015 之前的規範中被提到,它通常用於解釋 JavaScript 變數在記憶體中的一種運作形式
為什麼要把宣告放在前面?
學習過程中常常會聽到要先宣告變數賦予值,才能使用該變數,確實先宣告後取用很符合邏輯,但如果不這樣做會發生什麼事呢?先看看幾個範例
我們知道先宣告變數 a 賦予值為 1,再用 console.log(a)
就能印出 1,但反過來先使用再宣告變數,印出的值為什麼是 undefined
1 | console.log(a); // undefined |
這要說到 JavaScript 提升的觀念了,程式碼在瀏覽器執行時分為兩個階段:
- 創造階段 ( creation ) :這個階段會準備記憶體空間
- 執行階段 ( execution ) :這個階段才會實際執行程式碼
JavaScript 程式碼在執行過程中,會先進入創造階段準備好記憶體空間存放變數的值,再進入執行階段實際將值賦予到變數上
而以上程式碼因為在宣告變數前就使用變數,這時候 JavaScript 會開始尋找變數 a,找到確實有宣告變數 a,在創造階段就把變數 a 提升到前方,但因為變數還沒賦值,所以執行階段先執行的 console.log(a)
印出來的是 undefined,最後才執行變數 a 的賦值
所以以上程式碼其實是這樣的
1 | // 創造階段 |
而 JavaScript 這樣的特性就稱為「提升」,但以上這個範例說明的僅僅是 var 宣告變數時的狀態,如果遇到函式就不一樣了
相比於 var 宣告的變數,函式在提升上就多了不同的規則:
- 函式陳述式會提升完整的函式 :函式陳述式在創造階段的提升會包含整個完整的函式
- 函式陳述式優先 :函式陳述式會優先於變數的提升
等等,怎麼會突然冒出一個「函式陳述式」?
補充一下,宣告函式時通常有兩種方式:「函式陳述式」與「函式表達式」,這兩種方式寫法不同之外,在提升方式上也不同,先來看看以下範例了解兩者不同的地方
1 | // 函式表達式 |
什麼?為什麼執行程式碼的結果是「山羊」?不是「獅子」?這邊一樣用創造、執行階段來拆解程式碼,但要加入函式陳述式的規則:提升完整的函式、優先提升
1 | // 創造階段 |
透過以上程式碼可以看出兩者在提升上的不同,函式表達式會先宣告變數,再將函式賦予到變數上,而函式陳述式的提升又優先於變數,所以執行的反而是最後才賦值的函式表達式
因為這樣的不同所以在實戰中大多會擇一使用,但如果希望將函式宣告統一至於後方,那就只能使用函式陳述式方式宣告函式,這樣提升時才是完整的函式
let、const 的 TDZ
前面提到的 var 是 ECMAScript 早期的變數宣告方式,ES6 之後會避免使用 var 宣告,改用 let、const 新的宣告方式,當遇到不明問題時,新語法也能提供適當回饋提示開發者
前面提到用 var 先取值後宣告,會取得 undefined 的值、不會跳出錯誤訊息,但是用 let、const 如果先取值後宣告,就會出現類似以下的錯誤訊息,提示開發者無法在宣告前取得變數的值
這就是 let、const 宣告才有的「暫時性死區」( Temporal Dead Zone, TDZ ) 特性,在 let、const 宣告變數前方的區域就是 TDZ,如果試圖在這個區域寫入程式碼取得變數值,就會出現 ReferenceError
小補充:undefined 與 is not defined 的差異
undefined 與 is not defined 這兩者字面看起來似乎沒有差異,但經過上面的範例示範應該多少可以感覺到兩者的不同
相信看到這兩者應該還可以再聯想到:null,那這三者又有什麼差異呢?
這邊簡單的說明三者間的差異:
- undefined:是原始型別的一種,在宣告變數時系統預設的空值,開發者應避免手動為變數賦予 undefined 的值
- null:是原始型別的一種,開發者手動賦予的空值,需要抹除變數已被賦予的值時可以使用 null
- is not defined:是沒有宣告變數卻試圖取得該變數的值時,會跳出的錯誤訊息,這會讓後面的程式碼無法繼續執行,所以要及時修正才不會影響程式碼運作
結語
本篇文章介紹了有關 JavaScript 變數宣告、作用域及提升相關的內容,希望能記錄下我學習到的相關內容,同時也希望這篇分享能幫助正在學習 JavaScript 的朋友
那我們就下篇文章見囉 ~