iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 22
0
Modern Web

跟著 YDKJS 作者 Kyle Simpson 打造全新 JavaScript Mindset系列 第 22

[day21] YDKJS (Scope) : Hoisting ? let 會 Hoist 嗎 ?

tags: 鐵人賽
  1. Hoisting : Hoisting 並不是文件規範的詞彙
    • sample : 一個最常見的範例
    • sample : 另一個範例 function Hoisting
  2. 補充說明 Function Declaration
  3. let 會 Hoist 嗎 ?
  4. Spec.
  5. Kyle 的建議 : Variable Hoisting V.S. Function Hoisting
  6. 為什麼 Declaration 可以 hoisting , expression 不行?

Hoisting 並不是文件規範的詞彙

因為 JavaScript engine 執行時,沒有東西真正被 「Hoist」

Hoisting 是一種 隱喻,或是說對某一種「狀況」,我們慣例上用 Hoisting 這個字來稱呼它,
但其實, Hoisting 就是在探討 lexical scope 的問題。

一個最常見的範例

不要忘記,我們之前了解 有 compile time 和 Run time

Compile time (parsing):

  • line 3, line 4 有 Variable Declaration:
Execution Context(global) Memory
student
teacher

Run time :

  • line 1, line 2 有 expression statement(source reference):
Execution Context(global) Memory
studnet; // ?? student
teacher; // ?? teacher

我們把 expression 丟到 Execution Context,然後執行它之後:

Execution Context(global) Memory
studnet; // undefined student
teacher; // undefined teacher

因為還沒有賦值,所以是 undefined

大家可以回去看 day06 : undefined 表示我們有宣告這個變數,不過在這時候還 沒有賦值(no value).

  • line 3, line 4 有 expression statement(targent reference):

把值寫進 identifer

Execution Context(global) Memory
student : "you"
teacher : "Kyle"

寫進去以後再執行,就會印出值了:

studnet;
teacher;
Execution Context(global) Memory
studnet; // "you" student : "you"
teacher; // "Kyle" teacher : "Kyle"

假設不知道 JavaScript 會執行兩次的話 ⋯⋯

以上的步驟應該沒有問題,我們現在都知道 JavaScript 是兩步驟,

  1. 會有 Compile time(parsing) 先分析好我們的 Scope (之前提的規劃書)
  2. 才有 Run time ,這時候才真正執行 code (line by line)

以上的步驟叫做有 lexical scope 的語言。

註: 上面的步驟我有做成投影片,這次換成表格來試著說明看看。

投影片在這邊,都沒有什麼人點擊 QQ

但是,如果不知道呢? 要怎麼解釋 JavaScript 是直譯,但不會報錯的問題?

就出現這張圖,也是最多人 強行解釋 JavaScript 執行的方法 :

但事實上,你知道,我知道,獨眼龍也知道:「code 沒有被動過,沒有任何一行 code 受到傷害。」
那只是 JavaScript 的 Lexical Scope 而已。

這也就是我們上面表格的狀態。

Kyle 認為,可以用 Hoisting 這個字,但前提是你要知道它發生什麼事。

但是會讓更多人忽略 JavaScript 本質還是做 parsing, 然後 executing。
當你遇到一個「Hoisting」的問題,然後去 Stack Overflow 上面找答案,你得到的答案還是 「Hoisting」。 You learn nothing!

另一個範例 function Hoisting

Compile time (parsing):

  • line 3 有 Function Declaration, line 8 有 Variable Declaration
Execution Context(global) Memory
teacher (function)
otherTeacher (Variable)

補充說明 Function Declaration

  1. 宣告一個 Function Declaration :

    宣告一個 Function Declaration時,identifier 會是一個 function name,
    沒有帶括號()

    其意義是這個 identifier 的值是你的 function Code

    所以你今天在 console.log 一個 identifier (其中 identifier 儲存 function) ,
    這時候會出現真的 code,而不是執行 code。

    換句話說,你用 Function Declaration 寫個無窮迴圈,只要不執行,
    JavaScript 只是把你的 無窮迴圈code 放在記憶體,然後透過你指定的 identifier 當作便簽(label),方便後續使用。

    如果你的 Function Declaration 內還有其他 Declaration (無論是變數還是其他函式),compiler 的階段也只是單純紀錄並規劃 scope ,所以還是不會執行

  2. 執行一個 Function Expression :
    你必須很顯性的使用 expression , JavaScript 才會知道你執行它。

    執行時,會依據規劃書建立一個 Execution Context,也就是我們的 Scope a.k.a. 不同顏色的木桶。

    執行時,Function Expression 的 return 還是一個 JavaScript 定義的值 ,所以顯性的使用 identifer() 就是代表 一個 JavaScript 定義的值 (對比 identifer 代表一堆 真實的code )。

Run time :

  • line 1 : teacher()
Execution Context(global) Memory
teacher() teacher (function)
otherTeacher (Variable)

teacher() 會把 function teacher(){...} 拿出來執行,
這時候會創造一個 function teacher 的 Local Execution Context

function teacher() 的 Execution Context Memory
return "Kyle"; (沒有parameter)

執行之後知道, teacher() => "Kyle"
所以 line 1 會印出 "Kyle"

  • line 2 : otherTeacher()

otherTeacher() 會把 function otherTeacher(){...} 拿出來執行,
但是這時候發現找不到一個 function 叫做 otherTeacher

[day16] YDKJS (Scope) : Lexical Scope Review,Error 種類

有找到 variable

如果 variable 有找到,但你嘗試做些不可能的操作,這時候的 Error throw 結果稱作 TypeError

很顯然,對 Variable 當作 function 存取,就是一個 不可能的操作,所以此時會出現 TypeError

遇到 Error ,就跳出了,程式 GG。

如果假裝不知道會兩次 parsing :


我不知道用哪種會比較好懂,但至少我現在看到 code 被移動感覺很怪。

不過 Kyle 想強調, line 9 的 Function Expression。
很多人喜歡用這種方式宣告,後面是一個 arrow function,
這時候你就無法享受 function hoisting 的好處。

因為 Kyle 都把 Function Declaration 放在 code 的最下面,
這樣可以不用每次看 code 都要一直滾動。

(我:????)

多來幾個例子

  • Compile time (parsing):
Execution Context(global) Memory
teacher (Variable)
otherTeacher (Function)
function otherTeacher() 的 Execution Context Memory
teacher (Variable)
  • Run time:
    line 5 : console.log(teacher) 裡面的 teacher 會先找自己的 Scope ,
    這時候有變數,但沒有賦值,所以是 undefined

    大家可以回去看 day06 : undefined 表示我們有宣告這個變數,不過在這時候還 沒有賦值(no value).

let 會 Hoist 嗎

let 不會 hoist !?

很顯然,這一定是錯的,let 會。但是 為什麼 ?

如果 let 不會 hoist ,那應該是出現 reference Error ,但是出現的還是 TDZ (temporal dead zone) error.

[day16] YDKJS (Scope) : Lexical Scope Review,Error 種類

沒有找到 variable

如果 RHS (常用 source position ) 沒有找到 variable,特別強調找到 global Scope 都失敗,這樣的 Error throw 結果稱作ReferenceError .

更進一步解釋 let hosting

這時候,靈魂的審問再次出現 : 「 var , let(const)有什麼不一樣?」

答案是

  1. 使用 var 宣告,在這時候還沒有賦值,我們可以給他一個 undefined(initialize as undefined)。
  2. 使用 let(const) 宣告,在你 Execution 階段以前,任何人都不能碰它(don't initialize it),讓他保持 uninitialized (尚未初始化) .
  • 如果一個值被標註是 uninitialized (尚未初始化) ,你還是嘗試接觸它,那就會有一個錯誤: TDZ (temporal dead zone)

很早以前藏下去的圖:
[day06] YDKJS (Type) : 特殊值:undefined / undeclared / TDZ ? , NaN , 負數 0

為什麼我們需要 TDZ error ?

現在我們都知道,let 與 const 也有 hoisting。
但他們不會初始化為 undefined,而且在賦值之前試圖取值會發生錯誤(TDZ error)。

但是,

WHY?

  1. TC39 強迫讓你不要在宣告以前使用變數

  2. 以下是 Kyle 說的,我覺得比較有說服力:

    學術上,真正開發語言上的原因:
    1. 其實和 let 沒有任何關係
    2. TDZ error 是為了實作 const

    你使用一個 const 在 block scope 內部,
    如果這時候沒有限制它不能初始化 undefined,你在真正 const 賦予值以前,
    你可以 console.log 它,他會是 undefined (就像 var 的 hoisting)

    這就違背了 const 在學術上(Academically)的想法, const 只能有一個值。
    所以為了避免這個問題,所有在真正 assign 以前的接觸都直接給 Error,以保護 const 的學術性。

    然後 const 和 let 一起推出的,他們就想 「let 也這樣做好了。」
    That is the fact.

Spec.

  1. let, const 有 created lexical (compile time)。
  2. 但是要等 lexical binging (run time) 才真正綁定初始值 。(a.k.a. 不會 compile time 時就初始化為 undefined)

Kyle 的建議 : Variable Hoisting V.S. Function Hoisting

Kyle 的建議是

  1. Variable Hoisting (line 3) 容易讓別人誤解,建議永遠使用前都維持 先宣告,再使用 的習慣。
  2. Function Hoisting 大多數人都不會有誤解,可以使用

    有時候放到整份 code 最下面可以減少找 main code 的時間。

  3. const . let 也是 Variable,所以也是 建議永遠使用前都維持 先宣告,再使用 的習慣,這樣就不會碰到 TDZ 。

為什麼 Declaration 可以 hoisting , expression 不行?

  1. 文章重看
  2. can't hoist 就是 can't happen at compile time

expression 是 run time 的東西,當然不會在 compile time 執行,也就不會有 expression hoisting。

心得

還好沒有寫的和 huli 那篇 我知道你懂 hoisting,可是你了解到多深? 一樣。
但那篇文章也有同樣引用到 Kyle,所以下半部可能有些雷同(特別是有對話那邊 XD)。

huli 有問 Kyle 一些問題,如果好奇可以看一下 Kyle 的回答:
https://github.com/getify/You-Dont-Know-JS/issues/1375

其實 hoist 很多人都寫過,但是如果還是「想成把 code 提前」難免有些可惜,
下次試著畫畫看兩個階段的表格,然後解釋 hoisting ,就會慢慢體驗『hoist 是 compile time 的事』

明天開始 closure !!!


上一篇
[day20] YDKJS (Scope) : Advanced Scope
下一篇
[day22] YDKJS (Closure) : Closure 入門: persistent lexical scope referenced data
系列文
跟著 YDKJS 作者 Kyle Simpson 打造全新 JavaScript Mindset31

1 則留言

0
huli
iT邦新手 5 級 ‧ 2019-10-07 04:57:18

這篇寫得不錯,特別喜歡解釋 TDZ 那邊,因為要實作 const 然後順便把 let 帶進來,聽起來十分合理

然後這邊稍微解釋一下我問的那個問題,我想表達的其實是:「當我們說一個程式語言是 compiled language 的時候,不是表示『它一定要是 compiled language』,而只是在說『大多數的實作(或是比較合理的實作)是 compiled 的』」

因為語言本身不會規範你一定要 compiled 阿,所以聽起來應該合理吧?不過對 Kyle 來說他好像覺得不合理,因為 ECMAScript 的 spec 「暗示」了應該要有 compiled 這件事,所以 compiled 才是唯一合理的結果。

不過我後來覺得我們不是在討論同一個問題就是了,畢竟英文不是母語所以我問起來可能沒有到位,要再來來往往我也沒有把握能問得好,搞不好看起來會像跳針XD

我提的其實比較像是「文字上」或是「語言上」的定義,是跟實作完全無關的。舉例來說,你可以自己寫個 C++ 直譯器,也可以寫 JS 直譯器,也可以寫 JS 編譯器,JS 本身沒有限制你一定要用直譯還是編譯。

但 Kyle 提的是「在實作上」JS 如果沒有經過編譯這個階段根本不合理,因為有編譯的效率一定高於沒編譯,所以編譯是必須的,因此 JS 就是一定會有編譯。

只是這兩種答案其實可以相容啦,你可以認同文字上的定義就是這樣,也可以認同在實作上必須得這樣,所以我覺得兩種都對。而且後來我覺得光是編譯跟直譯的定義就可以弄的很複雜了,深究下去也沒什麼意義,所以重點還是要知道說 JS 執行前到底做了什麼處理。

Ashe Li iT邦新手 5 級‧ 2019-10-07 06:28:12 檢舉

哦哦,那應該是我誤會了,
我修改內文~

我要留言

立即登入留言