JavaScript var , let , const




在 ES6(ES2015) 之後宣告「變數」與「常數」,除了原本的 var 以外,還可以透過 let 與 const 做宣告,在本文中,我們會說明使用 var , let 和 const 的差異,並討論他們的作用域、變數初始化以及 hoisting 。

var 作用域

1
2
3
4
5
6
7
8
9
var Christmas = 'Merry Christmas and happy New Year!'

function greeting () {
var hello = "greeting";
console.log(hello); // greeting
}

greeting();
console.log(hello); // error: hello is not defined

使用 var 宣告的變數是以函式作為作用域的分界,範例中的 Christmas 宣告在函式之外所以視為全域作用範圍(global),而 hello 宣告在函式之內所以其作用範圍於該函數之內。當我們執行 greeting() 可以存取到 hello ,而在 function 外就無法獲得該變數內容,所以在全域範圍中會找不到 hello 而產生 ReferenceError 的錯誤。

var 變數可以重複宣告

JavaScript 允許重複宣告變數,不過實際上並不會進行重覆宣告,只會將其視為指派資料值:

1
2
3
4
5
var Christmas = 'Merry Christmas and happy New Year!'
var Christmas;
console.log(Christmas); // 'Merry Christmas and happy New Year!'
var Christmas = 'We wish you a merry Christmas.'
console.log(Christmas); // 'We wish you a merry Christmas.'

var 的問題

var 是以函式為變數作用域的分界,在一些使用了區塊語句: if , else , for , while 等等區塊語句中,使用 var 宣告的變數會曝露到全域作用範圍:

1
2
3
4
5
6
7
8
9
10
function greeting () {
var hello = "greeting";
}

if(true){
var say = "Christmas time";
}

console.log(hello) // hello is not defined
console.log(say) // 可存取

使用 var 宣告變數容易造成程式上的誤解:

1
2
3
4
5
6
7
8
var hello = "greeting";
var counts = 2;

if(counts > 1){
var hello = "Christmas time";
}

console.log(hello) // Christmas time

因為 counts > 1 成立,所以 hello 會被重新賦予新值,如果在其他程式碼也有使用到 hello 這個變數,可能會不小心就更動了變數的內容造成程式執行錯誤。

透過 ES6 加入的 letconst 來宣告,以區塊語句為分界的作用域,將會更明確且不易發生錯誤。

使用 let 宣告 hellosay ,本來在全域範圍中可以存取到的 say 變成 ReferenceError ,因為 letconst 是用大括號 {} 來區分作用域:

1
2
3
4
5
6
7
8
9
10
function greeting () {
let hello = "greeting";
}

if(true){
const say = "Christmas time";
}

console.log(hello) // hello is not defined
console.log(say ) // say is not defined

下面例子中的 hello 也只能在 {} 內進行存取:

1
2
3
4
5
6
7
8
let counts = 2;

if(counts > 1){
let hello = "Christmas time";
console.log(hello);// "Christmas time";
}

console.log(hello) // hello is not defined

let 不能重複宣告

使用 let 宣告的變數不能再其作用域中重複宣告,但可以更新其值:

1
2
let hello = "greeting";
hello = 'Merry Christmas and happy New Year!';

以下例子會輸出 error:Identifier ‘hello’ has already been declared:

1
2
let hello = "greeting";
let hello = 'Merry Christmas and happy New Year!';

如果同名的變數在不同的作用域就不會出錯:

1
2
3
4
5
6
7
8
let hello = "greeting";

if (true) {
let hello = 'Merry Christmas and happy New Year!';
console.log(hello);//"Merry Christmas and happy New Year!"
}

console.log(hello);// "greeting"

var 使用同名變數不會產生錯誤訊息,而使用 let 宣告的變數只能在同一個作用域中宣告一次,這種做法將可避免更多錯誤的產生。

Hoisting

JavaScript 會將變數宣告放到該 Scope 的最上層,並將該變數的初始值設為 undefined ,這種特性稱為「變數提升」 (Variables Hoisting):

1
2
console.log (Christmas); // undefined
var Christmas = 'Merry Christmas and happy New Year!'

上面程式碼等同於下面:

1
2
3
var Christmas;
console.log(Christmas); // undefined
Christmas = 'Merry Christmas and happy New Year!'

由於 JavaScript 的 Hoisting 特性,建議將變數宣告都放在 Scope 的最上方,養成先宣告完成後再使用的習慣,讓程式邏輯更清楚,也可以避免預期外的結果或錯誤發生。

Temporal dead zone(TDZ)

TDZ 為 ES6 的新用語,它的作用主要是用在 letconst 上; varundefined 來初始化,代表在宣告之前存取變數會得到 undefinedletconst 一樣會有 Hoisting,而 let 不會初始化變數的值,如果再宣告前存取會出現 ReferenceError

1
2
console.log (Christmas); // ReferenceError: Christmas is not defined
let Christmas = 'Merry Christmas and happy New Year!'

TDZ 表示一個尚未被初始化的狀態,有一個變數經過宣告後但未被初始化,此時存取它就會產生 ReferenceError 。下面例子中的變數 Christmas 會先被提升到函式的最上面;此時會產生 TDZ,如果程式流程未執行到 Christmas 的宣告語句時,就算是在 TDZ 作用的期間,此時存取 Christmas 就會出現 ReferenceError 錯誤。

1
2
3
4
5
6
7
let Christmas = 'Merry Christmas and happy New Year!';

(function() {
// 產生 TDZ
console.log(Christmas) // TDZ期間存取,Cannot access 'Christmas' before initialization
let Christmas = 'We wish you a merry Christmas.' // 對 Christmas 的宣告語句,這裡結束 TDZ
})();

如果先初始化值,再存取就不會出錯:

1
2
3
4
5
let Christmas = 'Merry Christmas and happy New Year!';

(function() {
console.log(Christmas) // 'Merry Christmas and happy New Year!'
}());

CONST

letconst 一樣是區塊作用域,唯一的差別是使用 const 宣告的變數代表常數 ,宣告的同時就要指定給值,並且不能重新賦予新值。

1
2
3
4
let hello = "greeting";
const say = "Christmas time";
hello = 'Christmas Eve'
say = 'Santa Claus' // TypeError: Assignment to constant variable.

重新給值就會出錯:

1
2
const say = "Christmas";
const say = "Christmas time"; // Identifier 'say' has already been declared

const 宣告的常數代表它是唯讀的,但並非代表這個參照到的值是不可改變的(immutable)。如果宣告的常數是一個參照類型的值,像是「物件」或是「陣列」,那裡面的值是可以改變的:

1
2
3
4
5
6
7
const person = {
name : 'Cheng Yi-Ting'
}
const test = []

person.name = 'CyTing'         
test[0] = '1'

如果只改變物件屬性的屬性值並不會出錯,但無法對整個物件分配新值:

1
2
3
4
5
const person = {
name : 'Cheng Yi-Ting'
}

person = {} // Assignment to constant variable.

for 迴圈中的 let

假設想要透過迴圈依序印出數字 0 到 4 ,並搭配 setTimeout 來達成每秒鐘輸出一個數字;過去我們可以透過在 for 迴圈中加入 IIFE 來完成,每次將當下的 i 作為參數傳入函式:

1
2
3
4
5
6
7
for( var i = 0; i < 5; i++ ) {
(function(x){
window.setTimeout(function() {
console.log(x);
}, 1000 * x);
})(i);
}

在 ES6 以後可以透過 let 來簡化,因為使用 {} 來區分作用域,所以每次 for 迴圈執行時,用 let 宣告的變數都會重新綁定一次,所以可以保留當下執行 i 的值:

1
2
3
4
5
for( let i = 0; i < 5; i++ ) {
window.setTimeout(function() {
console.log(i);
}, 1000 * i);
}

參考文獻

  1. https://dev.to/sarah_chima/var-let-and-const–whats-the-difference-69e

  2. https://tylermcginnis.com/var-let-const/

  3. https://eddychang.me/es6-tdz/

  4. https://ithelp.ithome.com.tw/articles/10225604

  5. https://ithelp.ithome.com.tw/articles/10185142