在 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++ ) { |