JS
JavaScript 系列 EP.03

Closure & Scope:
掌握 JS 的靈魂核心

為什麼函式執行完畢後,還能「記得」外部的變數?這不是魔法,而是 JavaScript 最強大也最令人困惑的機制:閉包與作用域。

作用域鏈
閉包應用
記憶體機制
✍️ Joseph Chen

Scope:你的變數住在哪裡?

Scope (作用域) 決定了變數的「可見性」。在 JavaScript 中,如果你在錯誤的地方呼叫變數,就會遇到經典的 ReferenceError

01

Global

全域作用域

整個程式碼都看得到,像空氣一樣。

02

Function

函式作用域

只有函式內部才看得到,外部無法窺探。

03

Block

區塊作用域 (ES6+)

if/for 大括號內部的變數,由 let/const 守護。

程式碼範例:

scope_example.js
const globalVar = "I'm Global";

function outer() {
  const outerVar = "I'm in Outer Function";
  
  if (true) {
    const blockVar = "I'm in Block Scope";
    console.log(globalVar); // ✅ 正常
    console.log(outerVar);  // ✅ 正常
  }
  
  console.log(blockVar); // ❌ ReferenceError: blockVar is not defined
}

outer();

一、Lexical Scope:決定身分的基因

JavaScript 採用的是 Lexical Scope (語彙作用域)。 簡單來說:一個變數的作用域是在「寫下程式碼的那一刻」就決定的,而不是在執行時決定。

為什麼這很重要?

因為它決定了函式在尋找變數時,會往哪一個「外層」去找。這個「尋找」的過程,我們稱為 Scope Chain (作用域鏈)

語彙作用域測驗
const name = "Global";

function sayName() {
  console.log(name);
}

function wrapper() {
  const name = "Wrapper";
  sayName(); // 這裡會印出什麼?
}

wrapper(); // 答案是 "Global"!

* 因為 sayName 被定義在全域,它的外層就是全域,即便在 wrapper 內部呼叫,它依然只會找它「定義時」的外層。


二、Closure:打破生命週期的契約

當一個函式可以「記得」並「存取」語彙作用域,即使這個函式是在原始作用域之外執行,這就是 Closure (閉包)

正常情況下,函式執行完畢後,記憶體會被回收。但閉包就像是一張「便利貼」,把函式執行時所需的環境變數給「黏」住了,不讓垃圾回收機制 (GC) 帶走它。

簡單的閉包範例
function createCounter() {
  let count = 0; // 這個變數被「黏」住了
  
  return function() {
    count++; // 每次呼叫都會存取同一個 count
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
雖然 createCounter 已經執行完了,但返回的匿名函式依然握著 count 的存取權。

三、閉包實戰:建立私有變數

在現代 JS 推出 `#private` 語法之前,閉包是唯一實現「資料封裝」的方法。這在寫模組或套件時非常重要。

Module Pattern (模組模式)
const BankAccount = (initialBalance) => {
  let balance = initialBalance; // 外部無法直接修改 balance

  return {
    deposit: (amount) => {
      balance += amount;
      console.log(`存入 ${amount},餘額 ${balance}`);
    },
    withdraw: (amount) => {
      if (amount > balance) return console.error("餘額不足");
      balance -= amount;
      console.log(`取出 ${amount},餘額 ${balance}`);
    },
    getBalance: () => balance
  };
};

const myAcc = BankAccount(1000);
myAcc.deposit(500); // 餘額 1500
console.log(myAcc.balance); // undefined (安全!)
優點:安全性

保護內部狀態不被外部隨意篡改,只能透過你定義的 API (methods) 來更動資料。

缺點:記憶體消耗

只要閉包函式還在,內部的變數就不會被回收。如果建立太多沒在用的閉包,會影響效能。


四、經典陷阱:迴圈中的閉包

這是一個面試必考題:為什麼 setTimeout 裡的 `i` 總是印出最後一個數字?

❌ 錯誤的寫法 (使用 var)

javascript
for (var i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(i); // 會印出 4, 4, 4
  }, 1000);
}

* var 是函式作用域,整個迴圈共用同一個 i。當 setTimeout 執行時,i 已經變成 4 了。

✅ 正確的寫法 (使用 let)

javascript
for (let i = 1; i <= 3; i++) {
  setTimeout(() => {
    console.log(i); // 會印出 1, 2, 3
  }, 1000);
}

* let 是區塊作用域,每次迭代都會產生一個新的 i,並被當前的閉包「黏」住。


總結:閉包的哲學

閉包不是什麼高深莫測的技巧,它是 JavaScript 為了讓函式變得「有生命」而設計的機制。理解了閉包,你就能從單純的「寫 Code」進階到「設計模式」,寫出更優雅、更具防護性的程式碼。

#JavaScript#Closure#Scope#LexicalScope#MemoryManagement#Frontend