JavaScript 變數與資料型別 ── 作用域(Scope)
簡介
在任何程式語言中,變數的可見範圍(Scope)都是決定程式結構與可維護性的關鍵因素。JavaScript 的作用域機制在 ES6 之前以 函式作用域 為主,之後加入了 區塊作用域(let、const),讓開發者可以更精細地控制變數的生命週期。
對於 初學者 來說,正確理解作用域可以避免「變數不見了」或「被意外覆寫」的錯誤;對 中階開發者,則是撰寫模組化、避免全域汙染、提升執行效能的基礎。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 JavaScript 作用域的運作方式。
核心概念
1. 作用域的分類
| 類型 | 說明 | 關鍵關鍵字 |
|---|---|---|
| 全域作用域 (Global) | 程式最外層的變數,任何地方都能存取。 | 無需 var/let/const 前綴,或直接掛在 window(瀏覽器)/global(Node)上 |
| 函式作用域 (Function) | 由函式宣告產生的範圍,var、function 會被封裝在此。 |
function foo(){ ... }、var x = 1; |
| 區塊作用域 (Block) | 由 {} 包起來的程式區塊,let、const 只在此區塊內有效。 |
if(){ ... }、for(){ ... }、{ ... } |
| 模組作用域 (Module) | ES6 模組 (import / export) 的私有範圍,檔案本身即是一個模組。 |
export const ...、import ... |
重點:
var永遠只能產生函式作用域,而let/const則會依照區塊產生作用域。
2. 變數提升(Hoisting)
在 JavaScript 中,宣告會被「提升」至所在作用域的最上方(但不會提升賦值)。以下示例說明不同關鍵字的提升行為:
// var 會被提升,且初始化為 undefined
console.log(a); // undefined
var a = 10;
// let / const 也會被提升,但在宣告之前存取會拋出 ReferenceError
// console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
注意:let / const 的提升稱為「暫時性死區(Temporal Dead Zone, TDZ)」,在變數宣告之前的任何存取都會錯誤,這有助於防止意外使用未初始化的變數。
3. 閉包(Closure)與作用域鏈
閉包是指 函式在執行時仍然可以存取其宣告時所在的外層作用域。這是 JavaScript 強大的特性,也常見於迭代器、私有變數等情境。
function makeCounter() {
let count = 0; // count 位於 makeCounter 的作用域
return function () { // 回傳的匿名函式形成閉包
count++; // 可直接存取外層的 count
console.log(count);
};
}
const counter = makeCounter(); // 產生閉包
counter(); // 1
counter(); // 2
在上述例子中,即使 makeCounter 已經執行完畢,count 仍然被回傳的函式保留,形成 持久的私有狀態。
4. 作用域的實務範例
範例 1:全域汙染的危害
// 不建議:直接在全域建立變數
var user = { name: 'Alice' };
function showUser() {
console.log(user.name);
}
// 其他程式碼意外改寫
user = null; // 造成 showUser 失效
解法:使用 IIFE(立即執行函式)或模組化:
(() => {
const user = { name: 'Alice' };
function showUser() {
console.log(user.name);
}
showUser(); // 正常運作
})();
範例 2:let 在迴圈中的正確使用
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 輸出: 0 1 2
若改成 var i,所有回呼會看到同一個 i(值為 3),因為 var 只有函式作用域。
範例 3:閉包實作私有變數
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有變數
return {
deposit(amount) {
balance += amount;
console.log(`存入 ${amount},餘額 ${balance}`);
},
withdraw(amount) {
if (amount > balance) {
console.log('餘額不足');
return;
}
balance -= amount;
console.log(`提領 ${amount},餘額 ${balance}`);
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 存入 500,餘額 1500
account.withdraw(2000); // 餘額不足
console.log(account.getBalance()); // 1500
此模式常用於 封裝、資料隱私,避免外部直接改寫 balance。
範例 4:區塊作用域與 const
{
const PI = 3.14159;
console.log(PI); // 3.14159
}
// console.log(PI); // ReferenceError: PI is not defined
const 確保變數在區塊內不會被重新賦值,也不會洩漏到外部。
範例 5:模組作用域(ES6)
math.js
export const add = (a, b) => a + b;
export const mul = (a, b) => a * b;
main.js
import { add, mul } from './math.js';
console.log(add(2, 3)); // 5
console.log(mul(4, 5)); // 20
// console.log(add); // ReferenceError: add is not defined (除非被匯入)
模組自動形成私有作用域,避免全域衝突。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 全域變數意外覆寫 | 使用 var 或未加宣告直接賦值會產生全域變數。 |
盡量使用 let / const,或在檔案最外層包裝 IIFE。 |
迴圈內的 var 關閉包 |
var 只會有一個共享的變數,導致回呼皆取得最後的值。 |
改用 let,或在 var 迴圈中使用立即函式產生新作用域。 |
| TDZ(暫時性死區)誤用 | 在 let / const 宣告之前存取會拋錯。 |
把使用位置移到宣告之後,或改用 var(不建議)。 |
| 不必要的嵌套作用域 | 過度使用 IIFE 造成程式可讀性下降。 | 只在需要避免全域汙染或建立私有狀態時使用。 |
| 模組循環相依 | ES6 模組相互引用時,可能產生未定義的匯入。 | 重構為單向依賴,或使用動態 import()。 |
最佳實踐
- 預設使用
const,只有在需要重新賦值時才改用let。 - 避免在全域建立變數,採用模組或 IIFE 包裝。
- 利用閉包封裝私有資料,提升程式的可維護性與安全性。
- 在迴圈或條件式內部使用
let/const,確保每次迭代都有獨立的變數。 - 遵循 ESLint 規則(如
no-var、prefer-const),自動捕捉作用域相關的錯誤。
實際應用場景
1. 前端表單驗證
在表單驗證函式中,常需要為每個欄位產生臨時的錯誤訊息變數。使用 區塊作用域 可以避免訊息被後續迴圈覆寫:
function validate(form) {
const errors = [];
for (let i = 0; i < form.elements.length; i++) {
const el = form.elements[i];
if (el.required && !el.value.trim()) {
const msg = `${el.name} 必填`;
errors.push(msg);
}
}
return errors;
}
2. Node.js 中的請求處理
每一次 HTTP 請求都應該有自己的上下文(如使用者資訊、請求 ID),使用 函式作用域 配合 閉包 形成獨立的狀態,避免不同請求之間相互干擾:
const http = require('http');
function createHandler() {
let requestId = 0;
return (req, res) => {
const id = ++requestId; // 每個請求都有唯一 ID
console.log(`Request #${id} - ${req.url}`);
res.end(`Your request ID is ${id}`);
};
}
const server = http.createServer(createHandler());
server.listen(3000);
3. Redux / Vuex 中的模組化狀態管理
在大型前端專案中,將狀態切分為多個模組,每個模組本身就是一個 模組作用域,確保 state、mutations、actions 不會互相衝突:
// store/user.js
export const state = () => ({
name: '',
token: null
});
export const mutations = {
setUser(state, payload) {
state.name = payload.name;
state.token = payload.token;
}
};
4. 測試環境的隔離
單元測試時,常需要在每個測試案例中建立乾淨的變數環境。利用 beforeEach 建立區塊作用域,可避免測試間的副作用:
describe('Array utils', () => {
let arr;
beforeEach(() => {
arr = [1, 2, 3];
});
test('push adds element', () => {
arr.push(4);
expect(arr).toEqual([1, 2, 3, 4]);
});
test('pop removes element', () => {
arr.pop();
expect(arr).toEqual([1, 2]);
});
});
總結
- 作用域 是 JavaScript 變數可見範圍的核心概念,分為全域、函式、區塊與模組四種。
var只產生 函式作用域,而let/const則提供 區塊作用域,配合 暫時性死區 防止未初始化的使用。- 閉包 讓函式可以保留外層變數,常用於私有資料、狀態封裝與函式工廠。
- 常見陷阱包括全域汙染、迴圈內的
var閉包、TDZ 錯誤等,最佳實踐則是預設使用const、避免全域變數、善用模組與閉包。 - 在 前端表單驗證、Node.js 請求處理、狀態管理、測試隔離 等實務場景中,正確的作用域設計能提升程式的可讀性、可維護性與安全性。
掌握了作用域的細節,你就能寫出 乾淨、可預測且易於擴充 的 JavaScript 程式碼,為日後的專案開發奠定堅實基礎。祝你寫程式愉快! 🚀