JavaScript
[JS] var, let, 以及 const 的區別

var, let, 以及 const 的區別

var, let, 以及 const 都是在 JavaScript 用來做變數宣告的保留字,在 JavaScript 早期只有 var,直到 ES2015 (ES6) 時才加入了 letconst

他們之間主要有 4 個不同:

  1. 作用域 (Scope) 不同: 在作用域上,var 可以是全域、也可以是以函式作為範圍;letconst 則是以區塊作為範圍。
  2. 重複宣告 (Declaration) 的差異: 在宣告上,var 可以被重複宣告,但是 letconst 則不行。letconst 在絕多數面向都是一樣,兩者的一大區別在於,用 let 宣告的變數可以重新賦值,但是用 const 的不行。
  3. 變量提升 (Hoisting) 不同: 在提升上,var 宣告的變數會自動初始化值為 undefined,因此在宣告前就使用變數,不會出現錯誤,而會是 undefined ;但是 letconst 宣告的變數則不會自動初始化,而是會進到暫時死區 (TDZ),因此在 letconst 宣告變數前使用該變數,會出現錯誤。
  4. for 迴圈的綁定 (Bind) 差異: var 在宣告變數前,不會綁定到 for 迴圈的上下文,而 letconst 會綁定到 for 迴圈的上下文。

作用域 (Scope) 不同

  • var 是函式作用域,let / const 是區塊作用域。
  • var 具有「函式作用域」,亦即在函式中宣告的變數,有效作用範圍會被限制在該函式中。但不具有區塊作用域,所以在區塊中宣告的變數,依然會作用到區塊之外,並不會被區塊限制住。
  • let / const 具有「區塊作用域」,亦即在區塊中宣告的變數,有效作用範圍會被限制在該區塊中。

在 ES6 前,並沒有區塊作用域(block scope)的概念,僅有全域(global scope)與函式作用域(function scope),所以 var 宣告的變數,具有函式作用域(function scope)的性質,代表 切分最小單位的有效範圍是 function

直到 ES6 後,新增區塊切分的概念,區塊是指 { }大括號範圍。 let / const 宣告的變數,才具有區塊作用域(block scope)的性質,切分最小單位的有效範圍是 block

而所謂的作用域就是「變數有效的作用範圍」,最大為全域作用範圍,意指變數有效範圍是全部範圍。

在區塊{ }中,用 var / let,宣告學生名為 Yuji 來看看結果。

/// 「var」 不受區塊限制,區塊外變數存取成功。///
 
{
  var studentName = "Yuji";
}
 
console.log(studentName);
//Yuji
///「let」會受區塊限制,區塊外變數存取失敗。///
{
  let studentName = "Yuji";
}
 
console.log(studentName);
//ReferenceError: corgiName is not defined

雖然 var 不受區塊限制,但會受到函式範圍限制,如下:

/// 「var」 受函式限制,函式外變數存取失敗。///
 
function callYuji() {
  var studentName = "Yuji";
}
 
console.log(studentName);
//ReferenceError: studentName is not defined
/// 即使用 var 宣告相同的變數名稱 studentName,但因為「 函式作用域 」,所以不會讓同名變數衝突。 ///
 
//將學生命名為Yuji,之後用 callYuji() 呼叫
function callYuji() {
  var studentName = "Yuji";
  console.log(studentName);
}
 
//將學生命名為Megumi,之後用 callMegumi() 呼叫
function callMegumi() {
  var studentName = "Megumi";
  console.log(studentName);
}
 
callYuji(); //Yuji
callMegumi(); //Megumi

由上述範例,可以看出限定作用域範圍有兩個好處:

  1. 避免同名變數的衝突。
  2. 能維持最小權限的原則,以避免變數資料被不當存取。

而過往用 var 宣告,僅有 function 函式範圍有這樣的好處,而如今用 let / const 宣告,就能讓上述好處發生在 if、for 等「 區塊作用域 」當中,藉此降低多人協作或大型專案的衝突情況。

重複宣告 (Declaration) 的差異

  • var 允許重複宣告,let / const 會出錯
var studentName = "Megumi";
var studentName = "Yuji";
console.log(studentName);
//Yuji
 
let studentName = "Megumi";
let studentName = "Yuji";
console.log(studentName);
//SyntaxError: Identifier 'studentName' has already been declared
 
const studentName = "Megumi";
const studentName = "Yuji";
console.log(studentName);
//SyntaxError: Identifier 'studentName' has already been declared

變量提升 (Hoisting) 不同

undefined 代表找不到值,已經被宣告,但尚未賦值。這邊結果為undefined ,即是代表:雖然我們看不見,但其實在 console.log(i) 之前,i 就已經被宣告了,只是尚未賦值。

///由於 var 直接變量提升,所以上面程式碼等同下面程式碼///
 
console.log(i);
var i = 5;
//undefined
 
var i;
console.log(i);
i = 5;
//undefined

這其中是因為 var 有著「提升(hoisting)」的特性,其實不僅是 var ,function 也有這個特性。以 var 宣告變數而言,「 變數提升 」簡而言之是: 在執行任何程式碼前,會把變數放進記憶體中,這樣的特點是,可以在程式碼宣告該變數之前使用它。

/// 因為 hoisting,所以可以在宣告變數前,就預先使用變數,所以可以寫完後一起宣告///
 
i = 2;
n = 3;
console.log(i + n);
var i;
var n;
// 5

在這樣的情況下,只需要有賦值就不會出錯,即使尚未宣告變數也不會出錯。

這樣會造成什麼問題?當養成了「 後宣告 」的習慣,如果最後忘了用 var 宣告呢?並不會出錯,只是變數會跑到全域中,變成全域變數,可能造成些 bug,像是:

/// 函式中沒有用 var 宣告,導致污染到全域 ///
 
var x = 1;
 
function addFunc(y) {
  x = 100;
  x = x + y;
}
 
addFunc(50);
console.log(x);
// 150,預期應該要是 1,但函式中的 x 跑出來了

但是 letconst 則不會,而是會進到暫時死區 (TDZ),因此在 letconst 宣告變數前使用該變數,會出現錯誤:

/// 先使用變數,後宣告會直接「出錯」,更嚴謹的養成先宣告,後使用///
 
console.log(i);
let i = 5;
//ReferenceError
 
i = 5;
console.log(i);
let i;
//ReferenceError

所以 習慣用 let 的開發者,通常會先宣告變數,而不會習慣先運算變數後宣告的情況,藉此降低開發出錯的機率 。

for 迴圈的綁定 (Bind) 差異

// 使用 var 宣告迭代變數
console.log("使用 var:");
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log("var 迴圈內的 i:", i);
  }, 1000);
}
console.log("var 迴圈外的 i:", i); // 可以存取 i,因為它被提升到外部作用域
 
// 使用 let 宣告迭代變數
console.log("使用 let:");
for (let j = 0; j < 3; j++) {
  setTimeout(function () {
    console.log("let 迴圈內的 j:", j);
  }, 1000);
}
// console.log("let 迴圈外的 j:", j); // 此行會拋出 ReferenceError,因為 j 只在迴圈內可見
 
// 使用 const 宣告迭代變數
console.log("使用 const:");
for (const k = 0; k < 3; k++) {
  setTimeout(function () {
    console.log("const 迴圈內的 k:", k);
  }, 1000);
}
// console.log("const 迴圈外的 k:", k); // 此行會拋出 TypeError,因為 k 只在迴圈內可見且不可重新賦值
  • 在這個範例中,使用 var 宣告的變數 i 被提升到外部作用域,因此迴圈外部仍然可以存取它。
  • 使用 let 宣告的變數 j只在迴圈內部可見,迴圈外部無法存取,這符合區塊作用域的規則。
  • 而使用 const 宣告的變數 k 也只在迴圈內部可見,且由於 const 宣告的特性,它無法重新賦值,因此無法在迴圈外部存取或修改。

結論:避免使用 var,使用 letconst 宣告變數。

References