# Day 28-30：JavaScript 設計模式：工廠函式與抽象工廠

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-01-05

---

影片資源
----

  

[![淺談 Javascript 設計模式 #2 創建：工廠函式與抽象工廠](https://storage.googleapis.com/papyrus_images/0b0f6a0552e9b4a9b51304ce99837fbf396cf2858da745e86eb383dc13ea0921.jpg)](https://www.youtube.com/watch?v=1IQxvGa90SY)

**影片來源**：[Alex 宅幹嘛 - 淺談 Javascript 設計模式 #2](https://www.youtube.com/watch?v=1IQxvGa90SY)

前言
--

工廠函式 (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 執行時：

1.  **建立執行上下文**：JavaScript 引擎建立新的 Execution Context
    
2.  **建立環境記錄**：建立 Declarative Environment Record 儲存 `privateCounter` 和 `changeBy`
    
3.  **回傳物件**：IIFE 回傳包含三個方法的物件
    
4.  **閉包保留參照**：三個方法持有對環境記錄的參照，`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 (
        <div>
          {(() => {
            if (!user) return <p>載入中...</p>;
            if (user.isVIP) return <h2>VIP 會員：{user.name}</h2>;
            return <p>一般會員：{user.name}</p>;
          })()}
        </div>
      );
    }
    

### 後端應用

#### 資料庫連線池管理

    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;
        },
      };
    })();
    

### 測試考量

私有方法無法直接測試，建議策略：

1.  只測試公開 API 的行為（黑箱測試）
    
2.  將複雜邏輯拆成獨立的純函式
    

    // 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 - 閉包](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Closures)
    
*   [MDN - IIFE](https://developer.mozilla.org/zh-TW/docs/Glossary/IIFE)
    
*   [TC39 ECMAScript 規範](https://tc39.es/ecma262)
    
*   [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/)
    
*   [SOLID Principles in JavaScript](https://blog.logrocket.com/solid-principles-javascript/)
    
*   [Factory Pattern vs Abstract Factory](https://www.geeksforgeeks.org/system-design/differences-between-abstract-factory-and-factory-design-patterns/)

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-28-30)*
