JavaScript 基礎:作用域與閉包

2026-01-26 08:26 | By justin | JavaScript
(Updated: 2026-01-26 08:27)

JavaScript 基礎:作用域與閉包

各位好!

這篇是 JavaScript 系列的第八單元,我們要學習作用域(Scope)與閉包(Closure)。這些是 JavaScript 中較進階但非常重要的概念。


什麼是作用域?

作用域決定了變數的「可見範圍」,也就是在程式的哪些地方可以存取該變數。

// 類比:作用域就像房子的房間
// 你在客廳放的東西,在臥室不一定看得到

let globalVar = "我在全域";  // 到處都看得到

function myFunction() {
  let localVar = "我在函式內";  // 只有函式內看得到
  console.log(globalVar);   // 可以存取全域變數
  console.log(localVar);    // 可以存取區域變數
}

myFunction();
console.log(globalVar);  // 可以存取
// console.log(localVar);  // 錯誤:無法存取函式內的變數

三種作用域

1. 全域作用域(Global Scope)

在任何函式外部宣告的變數。

// 全域變數
let userName = "Alice";
const API_URL = "https://api.example.com";
var oldStyle = "不建議使用 var";

function greet() {
  console.log(`Hello, ${userName}`);  // 可以存取全域變數
}

function changeUser() {
  userName = "Bob";  // 可以修改全域變數
}

greet();         // "Hello, Alice"
changeUser();
greet();         // "Hello, Bob"

// 在瀏覽器中,全域變數會附加到 window 物件
// console.log(window.userName);

2. 函式作用域(Function Scope)

在函式內部宣告的變數,只能在該函式內存取。

function example() {
  let x = 10;      // 函式作用域
  const y = 20;    // 函式作用域
  var z = 30;      // 函式作用域(var 也有函式作用域)

  console.log(x, y, z);  // 10 20 30
}

example();
// console.log(x);  // 錯誤:x is not defined

// 巢狀函式
function outer() {
  let outerVar = "外層";

  function inner() {
    let innerVar = "內層";
    console.log(outerVar);   // 可以存取外層變數
    console.log(innerVar);   // 可以存取自己的變數
  }

  inner();
  // console.log(innerVar);  // 錯誤:無法存取內層變數
}

outer();

3. 區塊作用域(Block Scope)

{} 大括號內宣告的變數(let 和 const)。

// if 區塊
if (true) {
  let blockVar = "區塊變數";
  const blockConst = "區塊常數";
  var notBlock = "var 沒有區塊作用域";

  console.log(blockVar);  // 可以存取
}

// console.log(blockVar);    // 錯誤:無法存取
console.log(notBlock);       // 可以存取(var 的問題)

// for 迴圈
for (let i = 0; i < 3; i++) {
  console.log(i);  // 0, 1, 2
}
// console.log(i);  // 錯誤:i 只存在於迴圈內

// 使用 var 的問題
for (var j = 0; j < 3; j++) {
  // ...
}
console.log(j);  // 3(j 洩漏到外層)

// 任意區塊
{
  let temp = "臨時變數";
  console.log(temp);  // 可以存取
}
// console.log(temp);  // 錯誤:無法存取

作用域鏈(Scope Chain)

JavaScript 會從內到外尋找變數。

let global = "全域";

function outer() {
  let outerVar = "外層";

  function middle() {
    let middleVar = "中層";

    function inner() {
      let innerVar = "內層";

      // 變數查找順序:
      // 1. inner 作用域
      // 2. middle 作用域
      // 3. outer 作用域
      // 4. 全域作用域

      console.log(innerVar);    // 在 inner 找到
      console.log(middleVar);   // 往外找到 middle
      console.log(outerVar);    // 再往外找到 outer
      console.log(global);      // 最後找到全域
    }

    inner();
  }

  middle();
}

outer();

變數遮蔽(Variable Shadowing)

內層變數會「遮蔽」外層同名變數。

let message = "外層訊息";

function showMessage() {
  let message = "內層訊息";  // 遮蔽外層的 message
  console.log(message);       // "內層訊息"
}

showMessage();
console.log(message);  // "外層訊息"(外層不受影響)

// 更複雜的例子
let x = 1;

function test() {
  let x = 2;  // 遮蔽外層的 x

  if (true) {
    let x = 3;  // 遮蔽函式層級的 x
    console.log(x);  // 3
  }

  console.log(x);  // 2
}

test();
console.log(x);  // 1

什麼是閉包?

閉包是指函式可以「記住」它被創建時的作用域環境。

// 基本範例
function outer() {
  let count = 0;  // outer 的區域變數

  function inner() {
    count++;  // 存取外層變數
    console.log(count);
  }

  return inner;  // 回傳內層函式
}

const counter = outer();  // counter 是 inner 函式
counter();  // 1
counter();  // 2
counter();  // 3

// 神奇的地方:outer() 已經執行完畢
// 但 count 變數仍然存在,被 inner 函式「記住」了

閉包的實用範例

私有變數

function createPerson(name) {
  let age = 0;  // 私有變數,外部無法直接存取

  return {
    getName() {
      return name;
    },
    getAge() {
      return age;
    },
    setAge(newAge) {
      if (newAge >= 0 && newAge <= 150) {
        age = newAge;
      }
    },
    birthday() {
      age++;
    }
  };
}

const person = createPerson("Alice");
console.log(person.getName());  // "Alice"
console.log(person.getAge());   // 0

person.setAge(25);
console.log(person.getAge());   // 25

person.birthday();
console.log(person.getAge());   // 26

// 無法直接存取 age 變數
// console.log(person.age);  // undefined

計數器

function createCounter(initialValue = 0) {
  let count = initialValue;

  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    get() {
      return count;
    },
    reset() {
      count = initialValue;
      return count;
    }
  };
}

const counter = createCounter(10);
console.log(counter.increment());  // 11
console.log(counter.increment());  // 12
console.log(counter.decrement());  // 11
console.log(counter.get());        // 11
console.log(counter.reset());      // 10

函式工廠

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplier(2);
const triple = multiplier(3);
const quadruple = multiplier(4);

console.log(double(5));      // 10
console.log(triple(5));      // 15
console.log(quadruple(5));   // 20

// 類似的範例:格式化器
function createGreeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}!`;
  };
}

const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
const sayHola = createGreeter("Hola");

console.log(sayHello("Alice"));  // "Hello, Alice!"
console.log(sayHi("Bob"));       // "Hi, Bob!"
console.log(sayHola("Carlos"));  // "Hola, Carlos!"

部分應用(Partial Application)

function add(a, b, c) {
  return a + b + c;
}

// 固定第一個參數
function partialAdd(a) {
  return function(b, c) {
    return add(a, b, c);
  };
}

const add5 = partialAdd(5);
console.log(add5(3, 2));   // 10
console.log(add5(10, 20)); // 35

// 更通用的版本
function partial(fn, ...fixedArgs) {
  return function(...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);
  };
}

const add10 = partial(add, 10);
console.log(add10(5, 3));  // 18

閉包的常見陷阱

迴圈中的閉包

// 錯誤範例
function createFunctions() {
  const functions = [];

  for (var i = 0; i < 3; i++) {
    functions.push(function() {
      console.log(i);
    });
  }

  return functions;
}

const funcs = createFunctions();
funcs[0]();  // 3(不是 0!)
funcs[1]();  // 3(不是 1!)
funcs[2]();  // 3(不是 2!)

// 為什麼?因為所有函式共用同一個 i 變數
// 當函式執行時,迴圈已經結束,i 已經是 3

// 解決方法 1:使用 let
function createFunctions() {
  const functions = [];

  for (let i = 0; i < 3; i++) {  // 用 let 代替 var
    functions.push(function() {
      console.log(i);
    });
  }

  return functions;
}

const funcs = createFunctions();
funcs[0]();  // 0
funcs[1]();  // 1
funcs[2]();  // 2

// 解決方法 2:立即執行函式(IIFE)
function createFunctions() {
  const functions = [];

  for (var i = 0; i < 3; i++) {
    functions.push((function(index) {
      return function() {
        console.log(index);
      };
    })(i));
  }

  return functions;
}

記憶化(Memoization)

使用閉包快取計算結果。

function memoize(fn) {
  const cache = {};  // 閉包保存的快取

  return function(...args) {
    const key = JSON.stringify(args);

    if (key in cache) {
      console.log("從快取取得");
      return cache[key];
    }

    console.log("計算中...");
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

// 費氏數列(低效版本)
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 記憶化版本
const memoizedFib = memoize(fibonacci);

console.log(memoizedFib(10));  // 計算中... 55
console.log(memoizedFib(10));  // 從快取取得 55

// 實用範例:API 請求快取
function createApiCache() {
  const cache = new Map();

  return async function fetchData(url) {
    if (cache.has(url)) {
      console.log("使用快取資料");
      return cache.get(url);
    }

    console.log("發送請求...");
    // const response = await fetch(url);
    // const data = await response.json();
    const data = { message: "模擬資料" };  // 模擬

    cache.set(url, data);
    return data;
  };
}

const cachedFetch = createApiCache();

模組模式(Module Pattern)

使用閉包建立模組。

const calculator = (function() {
  // 私有變數
  let history = [];

  // 私有函式
  function log(operation, result) {
    history.push({ operation, result, time: new Date() });
  }

  // 公開介面
  return {
    add(a, b) {
      const result = a + b;
      log(`${a} + ${b}`, result);
      return result;
    },
    subtract(a, b) {
      const result = a - b;
      log(`${a} - ${b}`, result);
      return result;
    },
    multiply(a, b) {
      const result = a * b;
      log(`${a} * ${b}`, result);
      return result;
    },
    divide(a, b) {
      if (b === 0) throw new Error("除數不能為 0");
      const result = a / b;
      log(`${a} / ${b}`, result);
      return result;
    },
    getHistory() {
      return [...history];  // 回傳副本
    },
    clearHistory() {
      history = [];
    }
  };
})();

calculator.add(5, 3);
calculator.multiply(4, 2);
console.log(calculator.getHistory());
// [
//   { operation: "5 + 3", result: 8, time: ... },
//   { operation: "4 * 2", result: 8, time: ... }
// ]

// 無法直接存取私有變數
// console.log(calculator.history);  // undefined

實戰練習

練習 1:銀行帳戶

function createBankAccount(initialBalance = 0) {
  let balance = initialBalance;
  const transactions = [];

  function record(type, amount) {
    transactions.push({
      type,
      amount,
      balance,
      time: new Date().toISOString()
    });
  }

  return {
    deposit(amount) {
      if (amount <= 0) {
        return "金額必須大於 0";
      }
      balance += amount;
      record("存款", amount);
      return `存款 ${amount} 元,餘額 ${balance} 元`;
    },
    withdraw(amount) {
      if (amount <= 0) {
        return "金額必須大於 0";
      }
      if (amount > balance) {
        return "餘額不足";
      }
      balance -= amount;
      record("提款", amount);
      return `提款 ${amount} 元,餘額 ${balance} 元`;
    },
    getBalance() {
      return balance;
    },
    getTransactions() {
      return [...transactions];
    }
  };
}

const account = createBankAccount(1000);
console.log(account.deposit(500));    // "存款 500 元,餘額 1500 元"
console.log(account.withdraw(300));   // "提款 300 元,餘額 1200 元"
console.log(account.getBalance());    // 1200

練習 2:事件訂閱器

function createEventEmitter() {
  const events = {};

  return {
    on(eventName, callback) {
      if (!events[eventName]) {
        events[eventName] = [];
      }
      events[eventName].push(callback);
    },
    off(eventName, callback) {
      if (!events[eventName]) return;
      events[eventName] = events[eventName].filter(cb => cb !== callback);
    },
    emit(eventName, ...args) {
      if (!events[eventName]) return;
      events[eventName].forEach(callback => callback(...args));
    },
    once(eventName, callback) {
      const wrapper = (...args) => {
        callback(...args);
        this.off(eventName, wrapper);
      };
      this.on(eventName, wrapper);
    }
  };
}

const emitter = createEventEmitter();

function onUserLogin(user) {
  console.log(`使用者 ${user} 已登入`);
}

emitter.on("login", onUserLogin);
emitter.on("login", (user) => {
  console.log(`歡迎 ${user}!`);
});

emitter.emit("login", "Alice");
// 使用者 Alice 已登入
// 歡迎 Alice!

emitter.off("login", onUserLogin);
emitter.emit("login", "Bob");
// 歡迎 Bob!(第一個監聽器已移除)

練習 3:節流與防抖

// 節流(Throttle):限制函式執行頻率
function throttle(fn, delay) {
  let lastTime = 0;

  return function(...args) {
    const now = Date.now();

    if (now - lastTime >= delay) {
      lastTime = now;
      return fn(...args);
    }
  };
}

// 防抖(Debounce):延遲執行,重複觸發會重置計時
function debounce(fn, delay) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

// 使用範例
const logMessage = (msg) => console.log(msg);

const throttledLog = throttle(logMessage, 1000);
const debouncedLog = debounce(logMessage, 1000);

// 節流:每秒最多執行一次
throttledLog("訊息 1");  // 立即執行
throttledLog("訊息 2");  // 被忽略
setTimeout(() => throttledLog("訊息 3"), 1100);  // 執行

// 防抖:停止觸發後才執行
debouncedLog("輸入 1");  // 不執行
debouncedLog("輸入 2");  // 不執行
debouncedLog("輸入 3");  // 1 秒後執行

let、const、var 的區別

// var:函式作用域、可重複宣告、變數提升
function testVar() {
  console.log(x);  // undefined(變數提升)
  var x = 10;
  var x = 20;      // 允許重複宣告
  console.log(x);  // 20
}

// let:區塊作用域、不可重複宣告、暫時性死區
function testLet() {
  // console.log(y);  // 錯誤:Cannot access before initialization
  let y = 10;
  // let y = 20;      // 錯誤:已經宣告過
  y = 20;            // 可以重新賦值
  console.log(y);    // 20
}

// const:區塊作用域、不可重複宣告、不可重新賦值
function testConst() {
  const z = 10;
  // z = 20;          // 錯誤:不可重新賦值

  // 但物件和陣列的內容可以修改
  const obj = { name: "Alice" };
  obj.name = "Bob";  // 可以
  // obj = {};        // 錯誤:不可重新賦值

  const arr = [1, 2, 3];
  arr.push(4);       // 可以
  // arr = [];        // 錯誤:不可重新賦值
}

// 建議:
// - 優先使用 const
// - 需要重新賦值時用 let
// - 不要使用 var

小結

這篇我們學習了:

  • 全域、函式、區塊作用域
  • 作用域鏈與變數遮蔽
  • 閉包的概念與應用
  • 私有變數與模組模式
  • let、const、var 的區別

下一步:

完成這篇後,你已經能夠: - 理解變數的可見範圍 - 運用閉包建立強大的功能 - 撰寫更安全的程式碼

前往下一篇:單元九:非同步 JavaScript

在那裡,我們會學習處理異步操作的方法。


0 留言

目前沒有留言

發表留言
回覆