一次說清楚 JavaScript 中宣告的各種提升行為(var、function、let/const)

提升(Hoisting)是 JavaScript 宣告變數後執行的行為,然而在不同狀況下會有不同的提升行為,我們在這篇文章一併釐清,並在後頭做比較。

序言

如果你沒有看過這兩篇可以先行點進去看(非常短)《用 1 分鐘簡單地了解 — JavaScript中 var 與 let 的主要差別》《你應該使用 let 而不是 var 的 3 個重要理由》

大綱

摘要

在 JavaScript 中有許多不同的宣告方式,由於不同時空背景所衍生的特性,我們常常會覺得提升行為只會發生在 ES5- 的 var 宣告中,許多時候我們可能忽略了其他的宣告也會造成提升,只是影響程度並不是那麼的大,所以不會特別去注意到。

為什麼變數宣告、提升在學習 JavaScript 中很重要呢?

Photo by Emily Morter on Unsplash

由於網頁程式中會執行大量的程式腳本,不論你分割開來在不同的檔案中,最終在 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

看起來好像沒什麼問題,可是當今天這個 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 吧,而且還不會報錯…。

Photo by Hermes Rivera on Unsplash

結論

來做個總結吧, Medium 不能畫圖表,所以我們來隨便歸結一下吧:

var 的提升行為導致宣告其實是做兩件事:

1~2之間該變數該變數為 undefined 狀態。

function 的提升行為其實是做一件事:

不會 call of undefined

let/const 的提升行為其實是做一件事:

寫在文末

許願文:希望 Medium 以後在撰寫程式碼的時候提供高亮的選項,真的不太想特別開 gist …。

If any interest, 👉 https://realdennis.me.

If any interest, 👉 https://realdennis.me.