本文 AI 產出,尚未審核

JavaScript 變數與資料型別 ── 作用域(Scope)

簡介

在任何程式語言中,變數的可見範圍(Scope)都是決定程式結構與可維護性的關鍵因素。JavaScript 的作用域機制在 ES6 之前以 函式作用域 為主,之後加入了 區塊作用域letconst),讓開發者可以更精細地控制變數的生命週期。

對於 初學者 來說,正確理解作用域可以避免「變數不見了」或「被意外覆寫」的錯誤;對 中階開發者,則是撰寫模組化、避免全域汙染、提升執行效能的基礎。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 JavaScript 作用域的運作方式。


核心概念

1. 作用域的分類

類型 說明 關鍵關鍵字
全域作用域 (Global) 程式最外層的變數,任何地方都能存取。 無需 var/let/const 前綴,或直接掛在 window(瀏覽器)/global(Node)上
函式作用域 (Function) 由函式宣告產生的範圍,varfunction 會被封裝在此。 function foo(){ ... }var x = 1;
區塊作用域 (Block) {} 包起來的程式區塊,letconst 只在此區塊內有效。 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()

最佳實踐

  1. 預設使用 const,只有在需要重新賦值時才改用 let
  2. 避免在全域建立變數,採用模組或 IIFE 包裝。
  3. 利用閉包封裝私有資料,提升程式的可維護性與安全性。
  4. 在迴圈或條件式內部使用 let / const,確保每次迭代都有獨立的變數。
  5. 遵循 ESLint 規則(如 no-varprefer-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 程式碼,為日後的專案開發奠定堅實基礎。祝你寫程式愉快! 🚀