特性
var
在 JavaScript Understanding The Wired Parts 課程中,有提到JS的 hoisting
特性,當語法解析器在解析程式碼時,他會把我們所宣告的變數都設置在一個記憶體位置,並先以 undefined
當作值儲存到記憶體當中,之後開始逐行讀取程式碼,當讀到了 var A = 'xxx'
的時候,再將 xxx
字串取代 undefined
賦值給 A
變數。
所以在 var A = 'xxx'
這行程式碼前印出 A
時,它還是 undefined
,讀取到了以後才賦值變成 xxx
。
1 | console.log(A) // undefined |
let 與 const
ES6的 let
和 const
無法在宣告前無法取用該值,否則會跳出 is not defined
錯誤。
1 | console.log(A) // Uncaught ReferenceError: A is not defined |
作用域
var
var的作用域為
Function Scope
只能以函式為變數作用域的分界,在一些使用了區塊語句(用花括號的語句)的像 if, else, for, while
等等區塊語句中,在這裡面用 var 宣告的變數仍然是會曝露到全域之中可被存取。
1 | function test(){ |
這對初學者容易造成誤解外,如果再搭配到隱藏的提升特性,整個程式碼經常會有出人意表的結果。在許多撰寫風格指引通常會提醒這點,而且叫你一定要把 var 語句寫在程式碼檔案的最上面。(甚至連 for 語句中的 var 宣告也要寫到最上面)
let 與 const
var的作用域為
Block Scope
如果使用了 let
或 const
來宣告,則是以區塊語句為分界的作用域,它會比較明確而且不易發生錯誤。一些之前對於 var
語句的麻煩撰寫風格,就可以不需要了。
1 | function test() { |
總之,不要再用 var
了,用 let
或 const
來取代它就是了。像我們有使用的 ESLint 檢查工具,一定出現會叫你不要使用 var
的訊息。
const 定義常數
const
針對是常數的定義,常數在一宣告時就必定要指定給值,不然會產生錯誤。而對於常數在 ES6 的定義是:
不可再指定 can’t re-assignment
指定的意思就是用等號 =
作指定運算,像下面這例子就是再指定值(或重覆指定值),所以會產生錯誤:
1 | const a = 10 |
宣告了一個常數,代表這個識別名稱的參照 reference是唯讀的 read-only,並不代表這個參照指定到的值是不可改變的 immutable。
意思是如果你宣告的常數是一個物件或陣列類型,裡面的值是可以作改變的。
1 | const a = [] |
所以對於物件、陣列、函式來說,使用const常數來宣告就可以,除非你有需要再指定這個陣列或物件的參照。
let使用於for語句
for 圓括號中的 let 變數仍然是在區塊作用域
for圓括號中
的第一個表達式,用 let
宣告變數時,是不是也會是被限制到 for
語句的區塊作用域中?答案是肯定的
,見下面的例子:
1 | for (let i = 0; i < 2; i++) { |
for 迴圈中的 let 變數會作重新綁定
這是 let
的特別之處,是由於區塊作用域造成的結果,在每次的 for迴圈
執行時,用 let
宣告的變數都會重新綁定 re-bind
一次。這是在 for 語句中 var 與 let 的差異
。
以下用程式碼直接看會比較容易的理解。這個改進主要是為了要解決在 for 語句中的閉包結構的問題。
使用 var
1 | for (var i = 0; i < 10; i++) { |
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 | for (let i = 0; i < 10; i++) { |
如果你使用了 let
而不是 var
,let
的變數除了作用域是在 for的{}區塊
中,而且會為每次循環執行建立新的詞彙環境(LexicalEnvironment),拷貝所有的變量名稱與值到下個循環執行,因此event queue
裡的十次console.log(i)
將會各自參考到不同的 i 變數,輸出預期的0~9。
模擬情況
1 | let k; |
let的陷阱 - Hoisting 和 臨時死區(TDZ)
要理解 let
, const
是否會被提升,可以用下面的簡單例子來看。第一個例子,是正常可以輸出 a
變數的值
1 | let a = 1; |
然而如果 let
不會提升,那麼這裡的 console.log(a)
應該還是會印出全域的 1
,但是執行時發現報錯,這是因為函數中 let a = 2
宣告的變數被提升到函數中區塊的最上面,進而產生TDZ,因此造成錯誤。
1 | let a = 1; |
對於TDZ,可以大概理解成這樣:
1 | let a = 1; |
在例子中的 IIFE 裡的函數作用域,變數 a
在作用域中會先被提升到函數區域中的最上面,但這時會產生TDZ,如果在程序流程還未運行到 a
的宣告語句時,算是在TDZ作用的期間,這時候取用 a
的值,就會拋出ReferenceError錯誤。
結論
let
和var
都有hoistinglet
有TDZ,var
沒有TDZ
不過說句實在話,let
有沒有 hoisting 都無所謂,程式還是那樣寫,因為即使是古早的var
我們也知道必須先宣告後使用,只要把握這個原則就沒問題了。
參考文章
撰寫風格建議
不要再用
var
來宣告變數,改用let
與const
,而且優先使用const
,除非需要再指定值才用let
。 var 的部份不要再用的理由,上面的內文已經有說明。const 可以用在物件、陣列與函式上,常數一宣告時就要指定值,犯錯的機會會減少很多。另外,JS 引擎也可以作最佳化。所以大概9成的情況都是用 const,只有像 for 迴圈語句或一些需要再指定值的情況才會用到 let。
並不是在區塊中或函式中區域的最上面來宣告變數/常數,而是在合理的位置,在變數/常數首次被使用時的上面一行來宣告變數。
現在的編譯器與 JS 引擎都已經做得很好,而且 let 與 const 都是區塊作用域,不用再擔心常數/變數會曝露到全域中的問題,提升特性如果你已經學過知道了,不要亂用也很難遇到。總之變數/常數在要用到前再宣告就行了,這是所謂的”合理的位置”。這會與 Douglas Crockford 大師所提的,一定要宣告在函式或程式檔案的最上面,這個撰寫風格的說法會不太一樣,不過當年也只有 var 可用,大師的想法自然有他的道理,但現在有了 let 與 const 就不用這樣做了。
重點歸納
- 若未使用
var
、let
、const
宣告,會使變數成為全域變數。 - ES6後,鼓勵使用
let
、const
取代var
。 let
在 for 迴圈時,每次循環都會重新綁定。let
與const
都是區塊作用域 ( block scope ),而var
是函式作用域( function scope )