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 留言
發表留言