一次說清楚 JavaScript 中宣告的各種提升行為(var、function、let/const)
提升(Hoisting)是 JavaScript 宣告變數後執行的行為,然而在不同狀況下會有不同的提升行為,我們在這篇文章一併釐清,並在後頭做比較。
序言
如果你沒有看過這兩篇可以先行點進去看(非常短):《用 1 分鐘簡單地了解 — JavaScript中 var 與 let 的主要差別》、《你應該使用 let 而不是 var 的 3 個重要理由》。
大綱
- var 宣告的提升行為
- function 宣告的提升行為
- let/const 宣告與暫時性死區
摘要
在 JavaScript 中有許多不同的宣告方式,由於不同時空背景所衍生的特性,我們常常會覺得提升行為只會發生在 ES5- 的 var 宣告中,許多時候我們可能忽略了其他的宣告也會造成提升,只是影響程度並不是那麼的大,所以不會特別去注意到。
為什麼變數宣告、提升在學習 JavaScript 中很重要呢?
由於網頁程式中會執行大量的程式腳本,不論你分割開來在不同的檔案中,最終在 script 標籤引入或是打包後都會變成共享同一個全域範疇,這時若發生非預期的提升或是全域污染,其導致的錯誤將會變得相當顯著,並且難以Debug。
var 宣告與提升
var 宣告是最常被問到的一個宣告方式,由於其函數範疇的特性,我們可以從這個議題發散至「閉包導致非同步印出最終值」、「提升導致印出undefined
」等等的相關議題。我們直接就後者討論 var 的提升行為。
由於函數作用域,以下範例直接撰寫立即執行函數:
(function(){
console.log(variable);
var variable=20;
}())
印出undefined
,為什麼呢?因為會提升嘛!
實際上該程式碼行為是這樣:
(function(){
var variable;
console.log(variable);
variable = 20;
}())
基本上 var 與提升常見到幾乎是「講到爛」的程度,值得一提的是儘管我們知道 var 會導致莫名的 undefined
行為,很多時候往往無法用肉眼避免這種事情發生。
我們舉一個常見的例子:
function parent(){
var name= 'Harry';
function child(){
var fullname = name + ' ' + 'Potter';
console.log(fullname);
}
return child
}var child = parent();
child();// Harry Potter
- parent function 存在一個變數 name
- child function 使用 parent 裡頭的變數 name
看起來好像沒什麼問題,可是當今天這個 child function 很大的時候…
function parent(){
var name= 'Harry';
function child(){
var fullname = name + ' ' + 'Potter';
console.log(fullname); //幾百行之後
var name = 'Dennis'; }
return child
}var child = parent();
child();// undefined Potter
哭哭饅頭
也是因此,我們用肉眼還是很難預防這種非預期的錯誤發生,所以在開發專案時我們可以透過 linter 來保護我們,例如ESLint中的 vars-on-top 。另外有一種開發模式(或是說開發原則),叫做「單一 var 原則」。
單一 var 原則 要求在這個 function scope 中開發者要將所有該定義的變數,一次在最上層的 var 中定義清楚:
function (){
var name='Dio' ,stand='Zawarudo',blabla='foo';
// below is your code;
}
未來在維護該程式碼時也是從上頭的單一 var 中定義變數,由此保證不會印出 undefined
。通常在註解也會寫到用到上層 scope 的變數,以避免後續開發者一樣不合就蓋台。
function 宣告與提升行為
function 這個關鍵詞亦是用來宣告,而其作用域也是函數作用域。
{
function test(){
console.log('callable');
}
}
test() //callable
只是它只被用來宣告函數,特別的是 function 宣告其實也會提升,而且與 var 是不同的喔!
greeting('Andy');function greeting(name){
console.log('Hello! ',name)
}
你猜怎麼著?竟然會印出 Hello! Andy。
如果我們用 var 來宣告函數呢?
greeting2('Andy');var greeting2 = function(name){
console.log('Hello! ',name)
}
直接吃 Exception
原因很簡單,其實就跟上面var提到的一樣,提升是依照var的規則嘛:
var greeting2;//這時 greeting2 是undefined greeting2('Andy');
greeting2 = function(name){
console.log('Hello! ',name)
}
ES6 的 let 與 const 宣告與提升行為
let 與 const 都是 ES6 提出的宣告方式,也是現代開發中比較常見的宣告變數方式。
雖然我們常說 let/const 可以取代掉 var 的那些奇異行為,不過也不代表它們不會 Hoisting ,它們的提升行為體現在一個名叫「暫時性死區」(TDZ)的行為中。
我們將上述的例子改為 let 舉例:
{
//Block scope
console.log(variable);
let variable = 20;
}
會出現 Uncaught ReferenceError,這個錯誤訊息中提到:
Cannot access ‘variable’ before initialization
看到這你也許也有點感覺,JavaScript 並不是那麼地直譯,其 let/const 宣告會讓區塊作用域開頭至這個宣告以前的區域無法操作這個變數。
這個暫時性死區的存在可以避免我們犯一些低級錯誤比如上例中的fullname:
function parent(){
var name = 'Harry';
function child(){
var fullname = name + ' ' + 'Potter';
console.log(fullname); // 幾百行之後
let name = 'Dennis';
}
return child;
}
var child = parent();
child();
比起印出 undefined
,他會直接 Reference Error,這類的暫時性死區讓 ES6 的宣告不是那麼的隨意,但也讓整個程式變得可預期一點。
我想你大概不希望 merge 了一個 PR 之後,發現頁面出現 undefined
吧,而且還不會報錯…。
結論
來做個總結吧, Medium 不能畫圖表,所以我們來隨便歸結一下吧:
var 的提升行為導致宣告其實是做兩件事:
- 到函數作用域開頭宣告
- 到原本宣告位置賦值
1~2之間該變數該變數為 undefined
狀態。
function 的提升行為其實是做一件事:
- 瞬間移動到函數作用域開頭宣告
不會 call of undefined
let/const 的提升行為其實是做一件事:
- 宣告那行至作用域開頭進入暫時性死區
寫在文末
許願文:希望 Medium 以後在撰寫程式碼的時候提供高亮的選項,真的不太想特別開 gist …。