1、什麼是Hoisting變數提升?

提升的意思就是,讓變數可以在宣告之前就使用


它是一種釐清 JavaScript 在執行階段內文如何運行的思路(尤其是在創建和執行階段)
然而,提升一詞可能會引起誤解:例如,提升看起來是單純地將變數和函式宣告,移動到程式的區塊頂端,然而並非如此。
變數和函數的宣告會在編譯階段就被放入記憶體,但實際位置和程式碼中完全一樣。
from MDN

1-1、JS分兩階段運作

(1)第一輪是掃瞄 - 建立期
1A - 註冊名稱(identifier)
1B - 進行初始化(檢查語法、建立變數)

(2)第二輪式執行 - 執行期
執行函數、賦值

> 情境算式
> var a = 1
> console.log(a)
>
> ----------
> 建立期         執行階段  
> var a         1A、1b
> 
> 執行期
> a = 1           2
> console.log(a)  2       


JS宣告變數兩階段運作
建立期間把所有程式碼掃過,並把名稱都設定好
執行期間才把值給出去

1-2、var宣告執行範例

1-2-1、情境一

> console.log(a)
> var a = 1
> console.log(a)

兩階段執行狀況

> 第一階段
> console.log(a)     # 不執行
> var a = 1          # 執行 1A、1B -> 已初始化,但是還沒賦值
> console.log(a)     # 不執行

> 第二階段
> console.log(a)     # 執行,印出undefined
> var a = 1          # 執行,賦值
> console.log        # 執行,印出1

1-2-2、情境二

> var a = 1
> var a
> console.log(a) 

兩階段執行狀況

> 第一階段
> var a = 1              # 執行 1a、1b
> var a                  # 執行 1a、1b
> console.log(a)         # 不執行

> 第二階段
> var a = 1              # 執行 2,給值變1
> var a                  # 執行 2,但是啥都沒給,所以忽略
> 
> console.log(a)         # 執行,印出a是1

1-3、let如何解決變數提升

let 怎麼解決變數提升 => 1b不執行

兩階段執行狀況

> 第一階段
> console.log(a)     # 不執行
> let a = 1          # 執行1a,不執行1b, 被一個罩子蓋住了(TDZ)
> 
> 第二階段
> console.log(a)     # 執行, 但是因為,1b沒執行,噴出referenceError(尚未初始化)

1-4、TDZ暫時死區(Temporal Dead Zone)

正常情況 let 的執行狀況

> 第一階段
> let a = 1          # 執行,不執行1B,並給一個蓋子
> console.log(a)     # 不執行

> 第二階段
> let a = 1          # 執行,把蓋子拿掉
> console.log(a)     # 執行,印出 a = 1


const/let也有變數提升,只是他們多了TDZ來防止初始化這件事
ES6中let/const宣告的變數被賦值之前不允許使用。 –

1-5、函式宣告順序

1-5-1、範例一

兩階段執行狀況

> 第一階段
> sayHi()                                    # 第一輪不執行
> const sayNyName = function sayHi() {       # 第一輪做宣告,但是1b沒做 
>     console.log()
> }

> 第二階段
> sayHi()                                    # 第二輪呼叫變數
> const sayNyName = function sayHi() {       # 第二輪做事,印出裡面的東西
>     console.log()                  
> }
>
> BUT!!!!!!!,上面這樣會出錯,會發生initialization的問題,原因就是TDZ的問題

1-5-2、範例二

JS執行function的順序,在function裡面,1a、1b、2都會一起做好

兩階段執行狀況

> 第一階段
> sayHi()                   # 不執行
> function sayHi() {        # 執行 - sayHi執行1a、1b   {}執行2 
>     console.log("hello") 
> }
 
> 第二階段
> sayHi()                   # 執行
>
> 印出 hello

=> 因爲第一階段{}會執行2(執行函數賦值),因此第二階段執行的時候,才可以成功執行

1-5-3、範例三

> foo = "outer bar"
> 
> function bar() {
>     foo = "inside bar" 
>     console.log(`inner : ${foo}`)
> }
> bar()
> console.log(`outer : ${foo}`)

Ans

> inner : inside bar
> outer : inside bar

原因詳解,今天不用變數宣告的話,會直接在window(全域)放一個foo變數,因此後宣告的foo就會直接變成inside bar

2、scope的概念

2-1、var = function scope

var 會被 function 包住,但是無法被 {}block 包住

2-2、let/const = block scope

let/const 只要有一個大括號就可以把他包住

Ps. 包住的意思是,變數只能在該範圍運作

2-3、情境題一

在 if 回圈裡面設定 const 會怎麼樣

> var age = 20
> if (age >= 18) {
>     const message = "未成年"          # is not defined
> } else {
>     const message = "已成年"          # is not defined
> }
> 
> console.log(message) -> 因為const被{}包住,所以噴會出is not defined

這一題解法很簡單,只要在if前面先宣告message

> var age = 20
> let message;               -> 先宣告,不要給值
> if (age >= 18) {
>     message = "未成年"
> } else {
>     message = "已成年"
> }
> 
> console.log(message) -> # 未成年,這樣就可以跑了,因為前面先宣告過

2-4、情境題二

在 function 裡面設定 var 、 let 會怎麼樣

> function hello() {
>     var a = 1
>     let b = 2
> }
> hello()
> 
> console.log(a)    會出錯 is not defined,因為var和let都被限定在function中
> console.log(b)    會出錯 is not defined,因為var和let都被限定在function中

2-5、全域變數

瀏覽器裡面有一個全域物件叫做 window
node 世界裡面也有一個全域物件叫做 global

> window.console.log("aaa")  
> window.alert("123")  

如果今天在網站上宣告一個 var ccc = 123
這個變數ccc會存進window裡面

但是,如果今天在網站上宣告一個 let ddd = 123
這個變數ddd 不會 存進window裡面


簡單的說var宣告會造成全域物件的污染



那如果我們直接使用賦值 -> age = 20 呢
假設window裡面原本沒有age這個變數
age = 20 這個動作會直接生出一個 變數age 丟在window環境裡面
這樣會造成很多問題,所以千萬不要輕易的宣告全域變數


3、let/const vs var

4-1、三個比較表

特性 var let/const
scope function scope block scope
變數宣告 不可
全域屬性 不會
變數提升 會(但是有TDZ壓住)