在 ES6(ES2015) 之後宣告「變數」與「常數」,除了原本的 var 以外,還可以透過 let 與 const 做宣告,在本文中,我們會說明使用 var , let 和 const 的差異,並討論他們的作用域、變數初始化以及 hoisting 。
var 作用域
1 | var Christmas = 'Merry Christmas and happy New Year!' |
使用 var 宣告的變數是以函式作為作用域的分界,範例中的 Christmas 宣告在函式之外所以視為全域作用範圍(global),而 hello 宣告在函式之內所以其作用範圍於該函數之內。當我們執行 greeting() 可以存取到 hello ,而在 function 外就無法獲得該變數內容,所以在全域範圍中會找不到 hello 而產生 ReferenceError 的錯誤。
var 變數可以重複宣告
JavaScript 允許重複宣告變數,不過實際上並不會進行重覆宣告,只會將其視為指派資料值:
1 | var Christmas = 'Merry Christmas and happy New Year!' |
var 的問題
var 是以函式為變數作用域的分界,在一些使用了區塊語句: if , else , for , while 等等區塊語句中,使用 var 宣告的變數會曝露到全域作用範圍:
1 | function greeting () { |
使用 var 宣告變數容易造成程式上的誤解:
1 | var hello = "greeting"; |
因為 counts > 1 成立,所以 hello 會被重新賦予新值,如果在其他程式碼也有使用到 hello 這個變數,可能會不小心就更動了變數的內容造成程式執行錯誤。
透過 ES6 加入的 let 或 const 來宣告,以區塊語句為分界的作用域,將會更明確且不易發生錯誤。
使用 let 宣告 hello 和 say ,本來在全域範圍中可以存取到的 say 變成 ReferenceError ,因為 let 和 const 是用大括號 {} 來區分作用域:
1 | function greeting () { |
下面例子中的 hello 也只能在 {} 內進行存取:
1 | let counts = 2; |
let 不能重複宣告
使用 let 宣告的變數不能再其作用域中重複宣告,但可以更新其值:
1 | let hello = "greeting"; |
以下例子會輸出 error:Identifier ‘hello’ has already been declared:
1 | let hello = "greeting"; |
如果同名的變數在不同的作用域就不會出錯:
1 | let hello = "greeting"; |
var 使用同名變數不會產生錯誤訊息,而使用 let 宣告的變數只能在同一個作用域中宣告一次,這種做法將可避免更多錯誤的產生。
Hoisting
JavaScript 會將變數宣告放到該 Scope 的最上層,並將該變數的初始值設為 undefined ,這種特性稱為「變數提升」 (Variables Hoisting):
1 | console.log (Christmas); // undefined |
上面程式碼等同於下面:
1 | var Christmas; |
由於 JavaScript 的 Hoisting 特性,建議將變數宣告都放在 Scope 的最上方,養成先宣告完成後再使用的習慣,讓程式邏輯更清楚,也可以避免預期外的結果或錯誤發生。
Temporal dead zone(TDZ)
TDZ 為 ES6 的新用語,它的作用主要是用在 let 和 const 上; var 用 undefined 來初始化,代表在宣告之前存取變數會得到 undefined 。 let 和 const 一樣會有 Hoisting,而 let 不會初始化變數的值,如果再宣告前存取會出現 ReferenceError :
1 | console.log (Christmas); // ReferenceError: Christmas is not defined |
TDZ 表示一個尚未被初始化的狀態,有一個變數經過宣告後但未被初始化,此時存取它就會產生 ReferenceError 。下面例子中的變數 Christmas 會先被提升到函式的最上面;此時會產生 TDZ,如果程式流程未執行到 Christmas 的宣告語句時,就算是在 TDZ 作用的期間,此時存取 Christmas 就會出現 ReferenceError 錯誤。
1 | let Christmas = 'Merry Christmas and happy New Year!'; |
如果先初始化值,再存取就不會出錯:
1 | let Christmas = 'Merry Christmas and happy New Year!'; |
CONST
let 和 const 一樣是區塊作用域,唯一的差別是使用 const 宣告的變數代表常數 ,宣告的同時就要指定給值,並且不能重新賦予新值。
1 | let hello = "greeting"; |
重新給值就會出錯:
1 | const say = "Christmas"; |
const 宣告的常數代表它是唯讀的,但並非代表這個參照到的值是不可改變的(immutable)。如果宣告的常數是一個參照類型的值,像是「物件」或是「陣列」,那裡面的值是可以改變的:
1 | const person = { |
如果只改變物件屬性的屬性值並不會出錯,但無法對整個物件分配新值:
1 | const person = { |
for 迴圈中的 let
假設想要透過迴圈依序印出數字 0 到 4 ,並搭配 setTimeout 來達成每秒鐘輸出一個數字;過去我們可以透過在 for 迴圈中加入 IIFE 來完成,每次將當下的 i 作為參數傳入函式:
1 | for( var i = 0; i < 5; i++ ) { |
在 ES6 以後可以透過 let 來簡化,因為使用 {} 來區分作用域,所以每次 for 迴圈執行時,用 let 宣告的變數都會重新綁定一次,所以可以保留當下執行 i 的值:
1 | for( let i = 0; i < 5; i++ ) { |