# Day 28-30:JavaScript 設計模式:工廠函式與抽象工廠 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-01-05 **URL:** https://paragraph.com/@gcake/day-28-30 ## Content 影片資源 影片來源:Alex 宅幹嘛 - 淺談 Javascript 設計模式 #2前言工廠函式 (Factory Function) 和抽象工廠 (Abstract Factory Pattern) 是創建型設計模式,核心目的是將「物件建立邏輯」與「物件使用邏輯」分離,常用於實踐 SOLID 原則中的開閉原則 (Open-Closed Principle) 和依賴反轉原則 (Dependency Inversion Principle)。核心概念對照工廠函式與抽象工廠的差異特性工廠函式 (Factory Function)抽象工廠 (Abstract Factory)定義一個函式負責建立並回傳物件,封裝建立邏輯建立一系列相關物件的工廠集合實作方式純函式,根據參數回傳不同配置的物件先定義工廠介面,再根據條件回傳對應的具體工廠生產對象單一產品類型產品族(一系列相關產品)抽象層級較低,抽象物件如何被創建較高,抽象工廠如何被創建使用時機需要建立多個相似物件,且屬性需動態決定需要支援多個平台/主題,每個平台都有一整套相關元件範例情境只生產漢堡生產整套速食套餐(漢堡+飲料+點心)工廠函式實作工廠函式是一個回傳物件的普通函式,不需要使用 new 關鍵字。基礎實作:從純函式到工廠函式// 步驟 1:純函式建立格式化字串 function getBurgerInfo(bread, meat, price) { return `${bread} + ${meat} 漢堡, 價格 $${price}`; } const info = getBurgerInfo('白麵包', '牛肉', 89); // "白麵包 + 牛肉 漢堡, 價格 $89" // 步驟 2:解構參數,接收物件 function getBurgerInfo(burger) { const { bread, meat, price } = burger; return `${bread} + ${meat} 漢堡, 價格 $${price}`; } const cheeseBurger = { bread: '白麵包', meat: '牛肉', price: 89, }; getBurgerInfo(cheeseBurger); // "白麵包 + 牛肉 漢堡, 價格 $89" // 步驟 3:使用閉包建立工廠函式 function createBurger(bread, meat, price) { return { getInfo() { return `${bread} + ${meat} 漢堡, 價格 $${price}`; }, }; } const cheeseBurger = createBurger('白麵包', '牛肉', 89); const chickenBurger = createBurger('全麥麵包', '雞肉', 79); console.log(cheeseBurger.getInfo()); // "白麵包 + 牛肉 漢堡, 價格 $89" console.log(chickenBurger.getInfo()); // "全麥麵包 + 雞肉 漢堡, 價格 $79" 封裝固定配方:實踐開閉原則// 基礎漢堡工廠 function createBurger(breadType, meatType, price) { return { bread: breadType, meat: meatType, price, getDescription() { return `${breadType}配${meatType}`; }, }; } // 大麥克專屬工廠(封裝固定配方) function createBigMac() { return createBurger('白麵包', '雙層牛肉', 119); } // 無敵豬肉堡工廠 function createMcPork() { return createBurger('芝麻麵包', '豬肉排', 59); } const bigMac = createBigMac(); const mcPork = createMcPork(); console.log(bigMac.getDescription()); // "白麵包配雙層牛肉" 符合開閉原則:當需要新增「勁辣雞腿堡」時,只需新增 createSpicyChicken() 函式,無需修改既有的 createBurger 邏輯。抽象工廠實作抽象工廠用於建立一系列相關的產品物件。建立產品工廠// 產品工廠:只負責建立物件 function createBurger(breadType, meatType) { return { type: 'burger', bread: breadType, meat: meatType, }; } function createDessert(name, calories) { return { type: 'dessert', name, calories, }; } function createDrink(name, size) { return { type: 'drink', name, size, }; } 具體工廠:麥當勞套餐function createMcDonaldsMeal(mealType) { if (mealType === 'classic') { const burger = createBurger('白麵包', '牛肉'); const dessert = createDessert('蘋果派', 250); const drink = createDrink('可口可樂', '中杯'); return { brand: 'McDonalds', mealType: 'classic', burger, dessert, drink, describe() { return `麥當勞經典套餐: ${burger.meat}堡 + ${dessert.name} + ${drink.name}`; }, }; } if (mealType === 'healthy') { const burger = createBurger('全麥麵包', '烤雞胸'); const dessert = createDessert('水果切盤', 80); const drink = createDrink('無糖茶', '大杯'); return { brand: 'McDonalds', mealType: 'healthy', burger, dessert, drink, describe() { return `麥當勞健康套餐: ${burger.meat}堡 + ${dessert.name} + ${drink.name}`; }, }; } return null; } 具體工廠:肯德基套餐function createKFCMeal(mealType) { if (mealType === 'classic') { const burger = createBurger('香酥麵包', '炸雞排'); const dessert = createDessert('蛋塔', 200); const drink = createDrink('百事可樂', '中杯'); return { brand: 'KFC', mealType: 'classic', burger, dessert, drink, describe() { return `肯德基經典餐: ${burger.meat}堡 + ${dessert.name} + ${drink.name}`; }, }; } if (mealType === 'spicy') { const burger = createBurger('辣味麵包', '香辣雞腿'); const dessert = createDessert('霜淇淋', 180); const drink = createDrink('七喜', '大杯'); return { brand: 'KFC', mealType: 'spicy', burger, dessert, drink, describe() { return `肯德基辣味餐: ${burger.meat}堡 + ${dessert.name} + ${drink.name}`; }, }; } return null; } 抽象工廠:統一介面function createRestaurantMeal(brand, mealType) { if (brand === 'McDonalds') { return createMcDonaldsMeal(mealType); } if (brand === 'KFC') { return createKFCMeal(mealType); } // 未來可擴充其他品牌 return null; } // 使用範例 const mcClassic = createRestaurantMeal('McDonalds', 'classic'); const kfcSpicy = createRestaurantMeal('KFC', 'spicy'); console.log(mcClassic.describe()); // "麥當勞經典套餐: 牛肉堡 + 蘋果派 + 可口可樂" console.log(kfcSpicy.describe()); // "肯德基辣味餐: 香辣雞腿堡 + 霜淇淋 + 七喜" 在此架構中:createRestaurantMeal 是抽象工廠createMcDonaldsMeal 和 createKFCMeal 是具體工廠每個具體工廠生產一個產品家族(漢堡 + 甜點 + 飲料)工廠函式與閉包閉包的角色工廠函式不一定需要閉包,但當需要私有變數時,閉包成為核心機制。概念定義在工廠函式中的作用閉包 (Closure)函式與其詞法環境的組合,內層函式可記住外層變數讓工廠函式建立的物件擁有私有狀態工廠函式 (Factory Function)負責建立並回傳物件的函式提供統一的物件建立介面IIFE 與閉包的結合立即呼叫函式表達式 (Immediately Invoked Function Expression, IIFE) 常與閉包結合,用於建立私有作用域。const counter = (function () { let privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment() { changeBy(1); }, decrement() { changeBy(-1); }, value() { return privateCounter; }, }; })(); console.log(counter.value()); // 0 counter.increment(); counter.increment(); console.log(counter.value()); // 2 counter.decrement(); console.log(counter.value()); // 1 運作原理當 IIFE 執行時:建立執行上下文:JavaScript 引擎建立新的 Execution Context建立環境記錄:建立 Declarative Environment Record 儲存 privateCounter 和 changeBy回傳物件:IIFE 回傳包含三個方法的物件閉包保留參照:三個方法持有對環境記錄的參照,privateCounter 不會被垃圾回收關鍵特性:counter 儲存的是 IIFE 回傳的物件(包含三個方法)privateCounter 存在於 IIFE 建立的詞法環境中三個方法透過閉包共享同一個 privateCounter使用場景前端應用防止全域變數污染// 使用 IIFE + 閉包封裝模組 const myApp = (function () { let count = 0; // 私有變數 return { increment() { count += 1; }, getCount() { return count; }, }; })(); myApp.increment(); console.log(myApp.getCount()); // 1 console.log(myApp.count); // undefined 建立私有狀態的元件const ShoppingCart = (function () { let items = []; let total = 0; function calculateTotal() { total = items.reduce((sum, item) => sum + item.price, 0); } return { addItem(item) { items.push(item); calculateTotal(); console.log(`已加入 ${item.name},總價: ${total}`); }, getTotal() { return total; }, clearCart() { items = []; total = 0; }, }; })(); ShoppingCart.addItem({ name: '漢堡', price: 100 }); ShoppingCart.addItem({ name: '可樂', price: 30 }); console.log(ShoppingCart.getTotal()); // 130 修正迴圈閉包陷阱// 問題:使用 var 在迴圈中綁定事件 for (var i = 0; i < 3; i += 1) { document.getElementById(`btn${i}`).addEventListener('click', function () { console.log(i); // 全部都會印出 3 }); } // 解法 1:使用 IIFE for (var i = 0; i < 3; i += 1) { (function (index) { document.getElementById(`btn${index}`).addEventListener('click', function () { console.log(index); // 正確印出 0, 1, 2 }); })(i); } // 解法 2:使用 let(推薦) for (let i = 0; i < 3; i += 1) { document.getElementById(`btn${i}`).addEventListener('click', function () { console.log(i); // 正確印出 0, 1, 2 }); } React 中的條件渲染function UserProfile({ user }) { return (
{(() => { if (!user) return

載入中...

; if (user.isVIP) return

VIP 會員:{user.name}

; return

一般會員:{user.name}

; })()}
); } 後端應用資料庫連線池管理const dbConnection = (function () { let pool = null; let isConnected = false; function createPool() { pool = require('pg').Pool({ host: 'localhost', database: 'mydb', }); isConnected = true; } return { getConnection() { if (!isConnected) { createPool(); } return pool; }, close() { if (pool) { pool.end(); isConnected = false; } }, }; })(); const conn = dbConnection.getConnection(); conn.query('SELECT * FROM users', (err, result) => { console.log(result.rows); }); 環境變數封裝const config = (function () { const env = process.env.NODE_ENV || 'development'; const secrets = { apiKey: process.env.API_KEY, dbPassword: process.env.DB_PASSWORD, }; return { isDevelopment() { return env === 'development'; }, getApiKey() { return secrets.apiKey; }, }; })(); if (config.isDevelopment()) { console.log('開發模式'); } 注意事項記憶體管理// 危險:閉包持有大量資料 const createHugeClosures = (function () { const hugeData = new Array(1000000).fill('x'); // 1MB 資料 return { getData() { return hugeData; // 閉包持續持有這 1MB }, }; })(); // 改善:釋放不需要的資料 const safe = (function () { let tempData = fetchHugeData(); const result = processData(tempData); tempData = null; // 釋放記憶體 return { getResult() { return result; }, }; })(); 測試考量私有方法無法直接測試,建議策略:只測試公開 API 的行為(黑箱測試)將複雜邏輯拆成獨立的純函式// helper.js - 可測試的純函式 export function complexCalculation(input) { return input * 2 + 1; } // module.js import { complexCalculation } from './helper.js'; const myModule = (function () { return { publicAPI(input) { return complexCalculation(input); }, }; })(); 現代替代方案使用情境IIFE + 閉包ES6 Module新專案模組化❌✅ 優先使用維護舊專案✅-單檔案內封裝✅-立即執行邏輯✅-實踐練習會員管理系統const membershipSystem = (function () { const members = {}; return { createMember(name) { if (members[name]) { throw new Error('會員已存在'); } members[name] = { totalPeriods: 0 }; }, addPeriods(name, periods) { if (!members[name]) { throw new Error('會員不存在'); } members[name].totalPeriods += periods; return this.calculateFee(name); }, calculateFee(name) { const { totalPeriods } = members[name]; const basePrice = 500; let total = totalPeriods * basePrice * 0.79; const discounts = Math.floor(totalPeriods / 5) * 200; return total - discounts; }, }; })(); membershipSystem.createMember('Alex'); membershipSystem.addPeriods('Alex', 3); membershipSystem.addPeriods('Alex', 2); console.log(membershipSystem.members); // undefined Class 語法對照class Burger { constructor(breadType, meatType, price) { this.bread = breadType; this.meat = meatType; this.price = price; } getInfo() { return `${this.bread} + ${this.meat} 漢堡, 價格 $${this.price}`; } } class BurgerFactory { static createBigMac() { return new Burger('白麵包', '雙層牛肉', 119); } static createMcPork() { return new Burger('芝麻麵包', '豬肉排', 59); } } const bigMac = BurgerFactory.createBigMac(); console.log(bigMac.getInfo()); 與函式版本的差異:Class 使用 this 存取實例屬性static 方法無需實例化,直接呼叫類別方法適合需要繼承關係的大型專案設計原則整合依賴反轉原則 (DIP)當呼叫方只認得工廠暴露的 API,而不依賴具體實作時,即達成 DIP。// 不好:直接依賴具體實作 const burger = new Burger('白麵包', '牛肉', 89); // 好:依賴工廠抽象 const burger = BurgerFactory.createBigMac(); 測試優勢在測試時可輕鬆替換成 mock 工廠:// test.js const mockBurgerFactory = { createBigMac() { return { getInfo: () => 'Mock Burger' }; }, }; 參考資源MDN - 閉包MDN - IIFETC39 ECMAScript 規範Airbnb JavaScript Style GuideSOLID Principles in JavaScriptFactory Pattern vs Abstract Factory ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates