JS 宣告變數: var/ let / const 差異
如果你像我一樣開始學 JS,多少會有個疑惑:
學習時是用 let / const 宣告變數,怎麼網路上查詢的 JS 程式碼,有些是用 var 宣告變數的呢?到底它們之間有什麼差異?
ES6 後,從 var 到 let / const
目前 JavaScript 主流是 ES6 版本,在這個版本中,是推薦使用 let 和 const 宣告變數,而在網上查詢的資料,很可能是 ES6 版本前撰寫的程式碼,那時候宣告變數的方式還是用 var 語法,才會導致上述疑惑的現象。
var 與 let / const ,主要有幾項差異:
- 作用域 (scope) 不同
- for 迴圈的綁定 (bind) 差異
- 變量提升 (hoisting) 不同
- 重複宣告的差異
這些改變的主要好處是: 讓 JS 變數的操作更加嚴謹,減少不直覺、出錯或重複宣告的可能性,藉此讓多人或個人大型開發更順利。
var 是函式作用域,let / const 是區塊作用域
在 ES6 前,並沒有區塊作用域(block scope)的概念,僅有全域(global scope)與函式作用域(function scope),所以 var 宣告的變數,具有函式作用域(function scope)的性質,代表切分最小單位的有效範圍是 function。
直到 ES6 後,新增區塊( { } 大括號範圍 )切分的概念, let / const 宣告的變數,才具有區塊作用域(block scope)的性質,切分最小單位的有效範圍是 blcok。
而所謂的作用域就是 「變數有效的作用範圍」,最大為全域作用範圍,意指變數有效範圍是全部範圍。
綜合整理:
- var 具有「函式作用域」,亦即在函式中宣告的變數,有效作用範圍會被限制在該函式中。但不具有區塊作用域,所以在區塊中宣告的變數,依然會作用到區塊之外,並不會被區塊限制住。
- let / const 具有「區塊作用域」,亦即在區塊中宣告的變數,有效作用範圍會被限制在該區塊中。
可以先來看看下方的例子。
1 | |
雖然 var 不受區塊限制,但會受到函式範圍限制,如下:
1 | |
由上述範例,可以看出限定作用域範圍有兩個好處:
- 避免同名變數的衝突。
- 能維持最小權限的原則,以避免變數資料被不當存取。
而過往用 var 宣告,僅有 function 函式範圍有這樣的好處,而如今用 let / const 宣告,就能讓上述好處發生在 if、for 等「 區塊作用域 」當中,藉此降低多人協作或大型專案的衝突情況。
var 與 let ,for 迴圈的綁定(bind)差異
讓我們用一個經典案例作為第二部分的開場。
1 | |
你覺得上述的程式執行的結果為何?
(A) 0 、 1 、 2 (B) 3 、 3 、 3
是不是 A 啊 ~ No ~ No ~ No ~ 其實是 B 哦。
我想很多沒學過 var 的人,包括我,都會直覺性的選擇 A ,因為如果我們學的是 let ,而將 var 替換成 let ,答案就會是 A ,這不是更符合 for 的效用嗎!!! 可惜事與願違,真正執行的結果會是 B。
上述預期結果應該是 0 1 2 ,卻莫名變成 3 3 3,正與宣告變數語法 var / let 有關。究竟怎麼了?
要解釋這個問題,要分成兩點討論:
- setTimeout() 的時間延遲,與 function 中 console.log(i) 的「 執行時間點 」。
- function 中 console.log(i) 的「 值怎麼來 」的。
首先來看第一點,當進入 for 迴圈時,宣告變數 var i = 0 ,並開始條件判斷,當 i < 3 時, i + 1,執行完後,接著需要等待 0.1 秒的時間,才會進行 setTimeout() 內的 function() { console.log( i ) ; }。
而 JavaScript 是「異步/非同步」語言,因此在等待執行 function() { console.log( i ) ; } 前的這 0.1 秒內,會先執行完已經能執行的 for 迴圈。
所以現在能理解第一點的結論: function 中,console.log( i ) 的執行時間點會在 for 迴圈執行完畢之後。
1 | |
澄清一下,目前為止所提的第一點,其實與 var 和 let 的差異是沒關係的,只是說明非同步和時間點的狀況。接著進入到第二點,console.log(i) 的值是怎麼來的,這就與 var 和 let 的差異有關了。
var 是函式作用域,這段 for 迴圈程式碼外沒有任何包覆,所以 var 所宣告的 i 會存在於全域 window (瀏覽器) 當中,且只會被綁定(bind)一次,也可以說是共用一個 instance。
加上 for 迴圈會先跑完,才 console.log( i ),所以最後 console.log( i ) 的值會才會是 3。
至於 let 則是「 區塊作用域 」每次 i 都會被紀錄在創造出來的區域中,更精確地說,是每次迭代都會建立一個新的環境(context),而這個環境會紀錄當下的變數 i 值,不會覆蓋掉上一個環境裡面的變數值,因此可以產生多個 i 值。。
可以看下方的示意圖,來便於理解「整體概念」,但細節上可能有出入。

讓們適時地整理一下,在 for 迴圈中,
- var 的情況只會綁定一次,而且不具區塊作用域,最終會只有一個值存在於全域中(以此例而言),也可以說只有一個 instance。
- let 的情況會發生重複綁定,而且具有區塊作用域,或者說多個紀錄變數的環境,最終會有多的值存在於 for 迴圈區塊中,也可以說會有多個 instance。
就實作而言,雖然僅有 var 的情況下也可以用「立即呼叫執行函式 IIFE」處理這樣的狀況,但較為複雜且不直覺,改用 let 後,就能簡單處理,且能更直覺地知道 for 迴圈結果啦!
1 | |
var 的提升與 let / const 不同
讓我們再次以一個問題開始 :
1 | |
你覺得上述的程式執行的結果為何?
- (A) 5
- (B) ReferenceError
- (C) undefined
可以想一下 XD
如果你跟我一樣比較少使用 var ,常使用 let 的話,應該會認為是(B) ReferenceError。
當然,不會那麼容易,印出來的結果其實是 (C) undefined。
非常不直覺啊…,因為 undefined 代表找不到值,已經被宣告,但尚未賦值,而 ReferenceError 則表示根本找不到 i ,尚未宣告。所以一般而言,會預期上述程式碼結果應該為「尚未宣告變數的ReferenceError」。
但以結果來說,卻是印出 undefined ,即是代表:雖然我們看不見,但其實在 console.log(i) 之前,i 就已經被宣告了,只是尚未賦值。
是不是頗違反直覺,因為 console.log(i) 前,根本沒有程式碼…。上述狀況,我們可以想像成下面這段程式碼來表示:
1 | |
這其中是因為 var 有著「提升(hoisting)」的特性,其實不僅是 var ,function 也有這個特性。
以 var 宣告變數而言,「 變數提升 」簡而言之是:在執行任何程式碼前,會把變數放進記憶體中,這樣的特點是,可以在程式碼宣告該變數之前使用它。
1 | |
在這樣的情況下,只需要有賦值就不會出錯,即使尚未宣告變數也不會出錯。
這樣會造成什麼問題?當養成了「 後宣告 」的習慣,如果最後忘了用 var 宣告呢?並不會出錯,只是變數會跑到全域中,變成全域變數,可能造成些 bug,像是:
1 | |
而 let 與 const 則不會,而是會進到 暫時死區 (TDZ),因此在 let 與 const 宣告變數前使用該變數,會出現錯誤:
1 | |
var 允許重複宣告,let / const 會出錯
最後再提一個能幫助防止開發錯誤或衝突的小地方,就是用 var 可以重複宣告同樣名稱的變數,而 let / const 如果重複宣告同名變數會出錯。
1 | |
這點對初學者或多人學做而言更能防止錯誤、容易尋找到錯誤所在。
var 與 let / const 差異總結
名詞解釋
- 區域作用域(Function-Scope):
let、const在區域內才能使用,離開宣告的區域就無法取得資料。 - 函式作用域(Block-Scope):
var依函示決定作用域。 - 向上提升(Hoisting):因為 JavaScript 是依順序執行,若在宣告變數前先執行變數,宣告的變數會提升到最上面,優先宣告變數。
統整
| 變數 | Function-Scope 函式作用域 / Global-Scope | Block-Scope 區域作用域 | Reassignable 重複定義 | Redeclarable 重複宣告 |
|---|---|---|---|---|
| const | X | V | X | X |
| Let | X | V | V | X |
| Var | V | X | V | V |
- var:ES5 新增的方法,屬於 Function Scope 全域變數,定義後的變數可以被修改,但後來很少人用。
- let: ES6 後新增的方法,屬於 Block Scope 區域變數,定義後的變數可以被修改
- const:ES6 後新增的方法,屬於 Block Scope 區域變數,定義後的變數不可以被修改
綜合來說就是 let/const 將宣告變數變得更加嚴謹,藉此增加易讀性、防止出錯,而最重要的 CTA ,我想就是:
在 JS 中,不要再用 var ,請都使用 let / const 宣告變數吧!