ES6+ 新特性:let 與 const 完全攻略
簡介
在 ES6(ECMAScript 2015)正式推出之前,JavaScript 只提供了 var 這一種宣告變數的方式。var 的作用域(scope)是 函式層級,且會因為 變數提升(hoisting) 而產生許多令人困惑的行為。
隨著前端框架與大型應用程式的興起,開發者對程式碼可讀性、可維護性與錯誤防護的需求日益提升。為了讓變數的生命週期更直觀、避免不小心重新賦值,ECMAScript 2015 引入了兩個全新的關鍵字:let 與 const。
本篇文章將從概念、語法、實作範例、常見陷阱與最佳實踐,完整說明 let / const 的使用方式,幫助 初學者 迅速上手,也讓 中階開發者 能在實務上更安心地寫出安全、易維護的程式碼。
核心概念
1. 作用域(Scope)—— 由 函式層級 變成 區塊層級
| 關鍵字 | 作用域類型 |
|---|---|
var |
函式層級(function scope) |
let / const |
區塊層級(block scope) |
區塊層級 指的是被
{}包住的程式區段,例如if、for、while、try...catch,甚至是單純的程式區塊。
範例 1:var 與 let 的作用域差異
function demoVar() {
if (true) {
var a = 10; // a 仍屬於函式作用域
}
console.log(a); // 10
}
function demoLet() {
if (true) {
let b = 20; // b 只在 if 區塊內有效
}
// console.log(b); // ReferenceError: b is not defined
}
demoVar();
demoLet();
var會把變數提升至函式最上方,導致在區塊外仍可存取。let(以及const)只在宣告所在的區塊內可見,提升(hoisting)仍會發生,但在宣告之前存取會拋出 ReferenceError(所謂的 TDZ,Temporal Dead Zone)。
2. 常數(Constant)—— const 讓值「不可重新指派」
const 並不代表「不可變」,而是 變數名稱不能再指向其他值。如果值本身是物件或陣列,仍然可以修改其內部屬性或元素。
範例 2:const 與物件/陣列
const PI = 3.14159; // 正確:宣告常數
// PI = 3; // TypeError: Assignment to constant variable.
const config = { api: '/v1' };
config.api = '/v2'; // ✅ 仍可修改屬性
// config = {}; // TypeError
const list = [1, 2, 3];
list.push(4); // ✅ 仍可變更內容
// list = []; // TypeError
重點:
const只保證「變數指向」不變,若需要真正不可變的資料結構,可使用Object.freeze()或第三方函式庫(如 Immutable.js)。
3. 暫時性死區(Temporal Dead Zone, TDZ)
即使 let / const 會被提升,但在宣告之前存取會拋出錯誤,這段區域稱為 TDZ。它避免了因變數提升而產生的意外行為,讓程式碼更易預測。
範例 3:TDZ 示範
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
4. 重複宣告的限制
- 同一區塊內 不能 同時使用
let/const重新宣告同名變數。 var可以重複宣告,這在大型程式碼中容易造成衝突。
let foo = 1;
// let foo = 2; // SyntaxError: Identifier 'foo' has already been declared
var bar = 1;
var bar = 2; // ✅ 允許(但不建議)
程式碼範例(實用示例)
以下提供 5 個 常見情境的範例,說明在實務開發中如何正確使用 let 與 const。
範例 1:迴圈內部的暫時變數
// 使用 let 讓每一次迭代都有自己的 i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// 若改用 var,所有回呼都會印出 3
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100); // 3, 3, 3
}
實務建議:在
for、for...of、for...in等迴圈中,盡量使用let,避免因閉包(closure)而產生的錯誤。
範例 2:函式參數的預設值與解構賦值
function createUser({ name = 'Anonymous', age = 0 } = {}) {
// name、age 為 const(不可重新指派)
const user = { name, age };
return user;
}
console.log(createUser({ name: 'Alice', age: 25 })); // { name: 'Alice', age: 25 }
console.log(createUser()); // { name: 'Anonymous', age: 0 }
- 使用 解構賦值 時,預設值可以讓函式更具彈性。
- 內部
user使用const,保證不會被意外改寫。
範例 3:避免全域汙染(IIFE + block)
// 傳統寫法:使用 IIFE(Immediately Invoked Function Expression)防止全域變數
(function () {
var temp = 'temp';
console.log(temp);
})();
// ES6 寫法:直接利用區塊作用域
{
let temp = 'temp';
console.log(temp);
}
// console.log(temp); // ReferenceError
重點:
let/const讓我們不再需要額外的 IIFE,程式碼更簡潔。
範例 4:深層物件的不可變(Object.freeze)
const SETTINGS = Object.freeze({
API_ENDPOINT: 'https://api.example.com',
TIMEOUT: 5000,
});
// 嘗試修改會失敗(在嚴格模式下會拋錯)
// SETTINGS.API_ENDPOINT = 'https://evil.com'; // TypeError in strict mode
Object.freeze結合const,可以建立真正的 只讀設定,在大型專案中相當常見。
範例 5:條件式區塊中的變數宣告
function calculate(value) {
if (value > 0) {
const result = Math.sqrt(value);
return result;
} else {
// const result = Math.abs(value); // SyntaxError: Identifier 'result' has already been declared
let result = Math.abs(value);
return result;
}
}
const只能在單一區塊內宣告一次,若需要在不同分支使用相同名稱,請改用let或在外層先宣告。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| TDZ 被忽略 | 在宣告前存取 let/const 會拋錯。 |
確保變數宣告在使用之前,或使用 var(不建議)。 |
| 誤以為 const = immutable | 只保證「指向」不變,物件仍可被修改。 | 需要不可變時使用 Object.freeze、deepFreeze 或 Immutable.js。 |
| 在迴圈外誤用 var | 產生意料之外的共享變數。 | 迴圈內部盡量使用 let,若需要外層值則使用 const。 |
| 重複宣告 | 同一區塊內 let/const 重複宣告會錯誤。 |
檢查變數命名或使用不同作用域。 |
| 全域污染 | 使用 var 宣告全域變數,容易被其他腳本覆寫。 |
預設使用 let/const,若必須全域,明確放在 window(或 globalThis)上。 |
最佳實踐清單
- 預設使用
const:除非明確需要重新指派,否則一律以const宣告。 - 僅在需要時使用
let:例如迴圈變數、條件式分支需要重新賦值的情況。 - 避免在同一作用域內混用
var:若必須保留舊程式碼,先行轉換為let/const。 - 利用 ESLint 之
prefer-const、no-var規則:自動檢測不當使用。 - 在大型物件或設定檔上使用
Object.freeze:保證不被意外改寫。
實際應用場景
1. 前端框架(React / Vue)中的 State 管理
React 函式元件常使用 useState、useReducer,內部狀態變數必須是 不可變 的。
const [count, setCount] = useState(0); // count 為 const
// 更新時不直接改寫,而是呼叫 setCount()
2. Node.js 後端服務的設定檔
// config.js
export const CONFIG = Object.freeze({
PORT: 3000,
DB_URI: process.env.DB_URI,
});
其他模組只要 import { CONFIG } from './config';,就能保證設定不會被改寫。
3. 測試環境中的 Mock 變數
在單元測試時,常需要暫時改寫全域變數。使用 let 可以在 beforeEach 中重新指派,測試結束後再恢復。
let fetchData;
beforeEach(() => {
fetchData = jest.fn().mockResolvedValue({ success: true });
});
test('should call fetchData', async () => {
await myModule.doSomething(fetchData);
expect(fetchData).toHaveBeenCalled();
});
4. 迭代演算法與資料處理
在資料清洗、排序等演算法裡,使用 let 來儲存暫時的索引或暫存值,確保每一次迭代不會相互干擾。
function bubbleSort(arr) {
const n = arr.length; // 不會改變
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交換
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
總結
let與const讓 變數的作用域 從函式層級升級為 區塊層級,減少意外共享與提升可讀性。const保證指向不變,但若值是物件或陣列,仍可修改其內容;需要真正不可變時,可結合Object.freeze。- TDZ(暫時性死區)是
let/const的安全機制,提醒開發者在宣告之前不要使用變數。 - 在實務開發中,預設使用
const,只有在需要重新指派時才使用let,並盡量避免var。 - 透過 ESLint、測試與程式碼審查,能夠養成正確使用
let/const的好習慣,提升專案的可維護性與穩定性。
掌握了這些概念與實作技巧後,你將能在 Modern JavaScript 的開發環境裡,寫出更安全、更具可讀性的程式碼。祝你寫程式愉快,持續探索 ES6+ 的更多新特性! 🚀