ES6 let & const

特性

var

JavaScript Understanding The Wired Parts 課程中,有提到JS的 hoisting 特性,當語法解析器在解析程式碼時,他會把我們所宣告的變數都設置在一個記憶體位置,並先以 undefined 當作值儲存到記憶體當中,之後開始逐行讀取程式碼,當讀到了 var A = 'xxx' 的時候,再將 xxx 字串取代 undefined 賦值給 A 變數。

所以在 var A = 'xxx' 這行程式碼前印出 A 時,它還是 undefined,讀取到了以後才賦值變成 xxx

1
2
3
console.log(A) // undefined
var A = 'xxx'
console.log(A) // xxx

let 與 const

ES6letconst 無法在宣告前無法取用該值,否則會跳出 is not defined錯誤。

1
2
console.log(A) // Uncaught ReferenceError: A is not defined
let A = 'xxx'

作用域

var

var的作用域為 Function Scope

只能以函式為變數作用域的分界,在一些使用了區塊語句(用花括號的語句)的像 if, else, for, while 等等區塊語句中,在這裡面用 var 宣告的變數仍然是會曝露到全域之中可被存取。

1
2
3
4
5
6
7
8
9
10
function test(){
var a = 10
}

if(true){
var b = 20
}

console.log(a) // a is not defined 存取不到
console.log(b) // 20 存取得到

這對初學者容易造成誤解外,如果再搭配到隱藏的提升特性,整個程式碼經常會有出人意表的結果。在許多撰寫風格指引通常會提醒這點,而且叫你一定要把 var 語句寫在程式碼檔案的最上面。(甚至連 for 語句中的 var 宣告也要寫到最上面)

let 與 const

var的作用域為 Block Scope

如果使用了 letconst 來宣告,則是以區塊語句為分界的作用域,它會比較明確而且不易發生錯誤。一些之前對於 var 語句的麻煩撰寫風格,就可以不需要了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function test() {
let a = 10
const a2 = 10
}

if (true) {
let b = 20
const b2 = 20
}

console.log(a) // a is not defined 存取不到
console.log(b) // b is not defined 存取不到,b被限制在if的{}中,外部環境無法存取

console.log(a2) // a2 is not defined
console.log(b2) // b2 is not defined

總之,不要再用 var 了,用 letconst 來取代它就是了。像我們有使用的 ESLint 檢查工具,一定出現會叫你不要使用 var 的訊息。


const 定義常數

const 針對是常數的定義,常數在一宣告時就必定要指定給值,不然會產生錯誤。而對於常數在 ES6 的定義是:

不可再指定 can’t re-assignment

指定的意思就是用等號 = 作指定運算,像下面這例子就是再指定值(或重覆指定值),所以會產生錯誤:

1
2
const a = 10
a = 20 // TypeError: Assignment to constant variable. 錯誤

宣告了一個常數,代表這個識別名稱的參照 reference唯讀的 read-only,並不代表這個參照指定到的值是不可改變的 immutable。
意思是如果你宣告的常數是一個物件或陣列類型,裡面的值是可以作改變的。

1
2
3
4
5
const a = []
a[0] = 1

const b = {}
b.foo = 123

所以對於物件、陣列、函式來說,使用const常數來宣告就可以,除非你有需要再指定這個陣列或物件的參照。


let使用於for語句

for 圓括號中的 let 變數仍然是在區塊作用域

for圓括號中的第一個表達式,用 let宣告變數時,是不是也會是被限制到 for 語句的區塊作用域中?答案是肯定的,見下面的例子:

1
2
3
4
5
6
7
8
for (let i = 0; i < 2; i++) {
console.log('in for statement: i', i)
// in for statement: i 0
// in for statement: i 1
// ( i變數被限制在for的"block scope"中,所以可以正常印出 )
}

console.log(i) // ReferenceError: i is not defined ( 全域則存取不到 )

for 迴圈中的 let 變數會作重新綁定

參考文章

這是 let的特別之處,是由於區塊作用域造成的結果,在每次的 for迴圈執行時,用 let 宣告的變數都會重新綁定 re-bind一次。這是在 for 語句中 var 與 let 的差異

以下用程式碼直接看會比較容易的理解。這個改進主要是為了要解決在 for 語句中的閉包結構的問題。

使用 var

1
2
3
for (var i = 0; i < 10; i++) { 
setTimeout(() => console.log(i), 1000)
}

JS中setTimeout屬於非同步函式,當JS引擎看到setTimeout時並不會等待一秒,而是先將它放入event queue等待執行,並且持續執行程式碼,也就是繼續執行第2、3、4…次for迴圈。此例總共迴圈了十次,所以event queue裡等待被執行的console.log(i)有十個,然而當for迴圈跑完時,i已經透過九次i++變成了10,因此累積的十次console.log(i)依序執行後參照到的 i 將都會是10,所以輸出的結果為十個10,而非我們預期的0~9。

使用 let

1
2
3
for (let i = 0; i < 10; i++) { 
setTimeout(() => console.log(i), 1000)
}

如果你使用了 let 而不是 varlet 的變數除了作用域是在 for的{}區塊中,而且會為每次循環執行建立新的詞彙環境(LexicalEnvironment),拷貝所有的變量名稱與值到下個循環執行,因此event queue裡的十次console.log(i)將會各自參考到不同的 i 變數,輸出預期的0~9。

模擬情況

1
2
3
4
5
let k;
for (k = 0; k < 10; k++) {
let i = k //注意這裡,每次循環都會建立一個新的i變數
setTimeout(() => console.log(i), 1000)
}

let的陷阱 - Hoisting 和 臨時死區(TDZ)

要理解 let, const是否會被提升,可以用下面的簡單例子來看。第一個例子,是正常可以輸出 a 變數的值

1
2
3
4
5
let a = 1;

(function() {
console.log(a); // 1
})()

然而如果 let 不會提升,那麼這裡的 console.log(a) 應該還是會印出全域的 1,但是執行時發現報錯,這是因為函數中 let a = 2宣告的變數被提升到函數中區塊的最上面,進而產生TDZ,因此造成錯誤。

1
2
3
4
5
6
let a = 1;

(function() {
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 2;
})()

對於TDZ,可以大概理解成這樣:

1
2
3
4
5
6
7
let a = 1;

(function() {
// 這裡產生 TDZ for a
console.log(a); // TDZ期間取用變數,產生Uncaught ReferenceError: a is not defined錯誤
let a = 2; // 對a的宣告語句,這裡結束 TDZ for a
})()

在例子中的 IIFE 裡的函數作用域,變數 a 在作用域中會先被提升到函數區域中的最上面,但這時會產生TDZ,如果在程序流程還未運行到 a 的宣告語句時,算是在TDZ作用的期間,這時候取用 a 的值,就會拋出ReferenceError錯誤。

結論

  • letvar 都有hoisting
  • let 有TDZ,var 沒有TDZ

不過說句實在話,let 有沒有 hoisting 都無所謂,程式還是那樣寫,因為即使是古早的var我們也知道必須先宣告後使用,只要把握這個原則就沒問題了。

參考文章


撰寫風格建議

  • 不要再用 var 來宣告變數,改用 letconst,而且優先使用 const,除非需要再指定值才用 let。 var 的部份不要再用的理由,上面的內文已經有說明。const 可以用在物件、陣列與函式上,常數一宣告時就要指定值,犯錯的機會會減少很多。另外,JS 引擎也可以作最佳化。所以大概9成的情況都是用 const,只有像 for 迴圈語句或一些需要再指定值的情況才會用到 let。


  • 並不是在區塊中或函式中區域的最上面來宣告變數/常數,而是在合理的位置,在變數/常數首次被使用時的上面一行來宣告變數。
    現在的編譯器與 JS 引擎都已經做得很好,而且 let 與 const 都是區塊作用域,不用再擔心常數/變數會曝露到全域中的問題,提升特性如果你已經學過知道了,不要亂用也很難遇到。總之變數/常數在要用到前再宣告就行了,這是所謂的”合理的位置”。這會與 Douglas Crockford 大師所提的,一定要宣告在函式或程式檔案的最上面,這個撰寫風格的說法會不太一樣,不過當年也只有 var 可用,大師的想法自然有他的道理,但現在有了 let 與 const 就不用這樣做了。

重點歸納

  • 若未使用varletconst宣告,會使變數成為全域變數。
  • ES6後,鼓勵使用 letconst 取代 var
  • let 在 for 迴圈時,每次循環都會重新綁定。
  • letconst 都是區塊作用域 ( block scope ),而 var 是函式作用域( function scope )

資料來源

eyesofkids - Day 05: ES6篇 - let與const
六角學院 - Vue出一個電商網站