考慮下面的這三句代碼和對應的報錯信息:
假設寫這個代碼的人一開始不知道 ES6 里新增的構造函數不能省略 new,于是第一行寫錯了。然后第二行嘗試重新聲明一次,結果又報錯說重復聲明了。那干脆不聲明,直接賦值總行吧,結果又報錯說 map 未定義。
這三個報錯直接對應規范里的下面三條規則(并附通俗解釋):
23.1.1.1 Map()
1. If NewTarget is undefined, throw a TypeError exception.
解釋:Map() 不帶 new 不能被調用。
15.1.11 GlobalDeclarationInstantiation()
5.b. If envRec.HasLexicalDeclaration(name) is true, throw a SyntaxError exception.
解釋:map 已經被聲明過,不能重復聲明。
8.1.1.1.5 SetMutableBinding()
4. If the binding for N in envRec has not yet been initialized, throw a ReferenceError exception.
解釋:map 處于已經聲明但未初始化的狀態,這種狀態不能通過 = 賦值。
第二個報錯的疑問點:第一次聲明等號右側報錯了,map 怎么會聲明成功呢?
一段代碼在被真正的執行前,會有個專門用來聲明變量的過程,俗語常把這個過程稱為預解析/預處理。無論是用 var 還是用 let/const 聲明的變量,都是在這個過程里被提前聲明好的,俗語常把這種表現稱為 hoisting。只是 var 和 let/const 有個區別,var 變量被聲明的同時,就會被初始化成 undefined,而后兩者不會。
第三個報錯的疑問點:沒有初始化我用 = 給它初始化還不行嗎?還有為什么報錯是 “map 未定義”?
首先說 “map 未定義”是 V8 的報錯信息不友好,正確的報錯信息應該是 “map 未初始化”。
規范規定一個已經聲明但未初始化的變量不能被賦值,甚至不能被引用,代碼示例里第三句即便只寫一個 map 也會報一樣的錯。規范里用來聲明 var/let 變量的內部方法是 CreateMutableBinding(),初始化變量用 InitializeBinding(),為變量賦值用 SetMutableBinding(),引用一個變量用 GetBindingValue()。在執行完 CreateMutableBinding() 后沒有執行 InitializeBinding() 就執行 SetMutableBinding() 或者 GetBindingValue() 是會報錯的,這種表現有個專門的術語(非規范術語)叫 TDZ(Temporal Dead Zone),通俗點說就是一個變量在聲明后且初始化前是完完全全不能被使用的。
因為 var 變量的聲明和初始化(成 undefined )都是在“預處理”過程中同時進行的,所以永遠不會觸發 TDZ 錯誤。let 的話,聲明和初始化是分開的,只有真正執行到 let 語句的時候,才會被初始化。如果只聲明不賦值,比如 let foo,foo 會被初始化成 undefined,如果有賦值的話,只有等號右側的表達式求值成功(不報錯),才會初始化成功。一旦錯過了初始化的機會,后面再沒有彌補的機會。這是因為賦值運算符 = 只會執行 SetMutableBinding(),并不會執行 InitializeBinding(),所以例子中的 map 變量被永遠困在了 TDZ 里。
其實我舉的這個例子已經在 Firefox、Chrome、Node 的 bug 平臺上都被反應過了。Firefox 的 JS 引擎為了消除這種奇怪的表現,專門針對 shell 環境(包括 Firefox 中的控制臺)做了特殊處理,當 let/const 語句等號右側的表達式求值發生錯誤后,引擎會把它初始化成 undefined:
如果是 js shell 的話,還能看到一段解釋信息,表明這樣做其實是違反規范的:
讀到現在,有同學就問了:“就因為這個就不讓在控制臺里用 let/const?我以后記得加 new 不就得了”。等號右邊的表達式報錯其實有很多種情況,比如某個屬性意外成了 undefined,比如右側的函數調用本身報錯了,都有可能,出錯其實挺常見的。
而且除了這種因報錯導致你不得不重新聲明一次的情況,還有一些情況是你主動想重復聲明的。比如我們經常在控制臺里寫代碼都是想最終產出一段代碼的,但你寫的時候是一句是一句寫的,寫一句回車執行,沒問題的話,按下上箭頭,然后按 shift+enter,換行后寫第二句,可能最終完成需要十來句。如果其中某一句用到了 let/const,第二次執行的時候就會報錯,然后你只能刷新頁面了。
不用 let/const 那用啥呢?用 var 或者直接用賦值語句都可以,依情況而定。而且本文的觀點并不是絕對的,很多情況下是可以用 let/const 的,比如你的聲明語句是寫在一個函數里的,比如你從別的地方復制了一個腳本(搶月餅?),只需要在控制臺粘貼執行一次,不用修改,這些情況用什么都可以。
文章列表