設計模式:原型模式 (Prototype)

8 分鐘
軟體設計設計模式OOP

Prototype 模式透過複製既有物件來建立新物件。不必把初始化流程再走一遍,而是直接從現有的實例出發,再從那裡微調。

它最適合這種情況:物件初始化的成本很高,而新物件多半只是既有物件的細微變化。


意圖與用途

核心想法:複製一份既有物件,再針對副本做些小調整。

常見的使用情境:

  • 設計工具中複製圖層
  • 遊戲引擎中複製物件模板 (子彈、各種 NPC 類型)
  • 設定頁面中提供預設值,讓使用者在其基礎上修改

淺層與深層複製

這是使用 Prototype 時最需要留意的地方。

淺層複製:只複製最外層的屬性。內層的物件、陣列仍然由原件與副本共用。

TypeScript
const original = { a: 1, b: { c: 2 } };
const shallow = { ...original }; // 淺層複製

shallow.b.c = 99;
console.log(original.b.c); // 9 —— 者指向同一個物件!

深層複製:連內層的物件都一起複製,兩個實例完全獨立。

TypeScript
const deep = JSON.parse(JSON.stringify(original)); // 簡單的深層複製
deep.b.c = 99;
console.log(original.b.c); //  —— 全

JSON.parse(JSON.stringify(...)) 簡單但有限 —— 無法處理 undefinedDate、函式,也無法處理循環引用。物件一旦複雜,正確的做法是自己寫一個 clone() 方法。


實作範例:圖形編輯器

TypeScript
interface Cloneable {
  clone(): this;
}

class Shape implements Cloneable {
  constructor(
    public type: string,
    public x: number,
    public y: number,
    public style: { color: string; strokeWidth: number },
  ) {}

  // 深層複 —— tyle 物件也一起複製
  clone(): this {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), {
      ...this,
      style: { ...this.style },
    });
  }

  moveTo(x: number, y: number): this {
    this.x = x;
    this.y = y;
    return this;
  }
}

const original = new Shape('circle', 10, 20, { color: 'red', strokeWidth: 2 });

// 複製一份,移到新位置
const cloned = original.clone().moveTo(50, 60);

console.log(original.x, original.y); // 10, 2 —— 有被改到
cloned.style.color = 'blue';
console.log(original.style.color); // 'red —— 全,是各自獨立的副本

實務應用

適用時機

  • 物件建立成本高或很吃資源,而新物件只需要少量修改
  • 需要對某個物件的狀態拍下快照,未來再從這個快照分支出去

注意事項

  • 物件內部含有引用型別時,必須決定要做淺層還是深層複製
  • clone() 方法要處理所有內部狀態,包括繼承而來的屬性

總結

Prototype 讓你不必知道物件真正的類別,就能直接複製一個現有實例。它常被用於預設物件的複製、物件模板、狀態快照,以及物件池的初始化。

永遠對淺層複製保持警覺:只要物件帶有引用型別的屬性,就應該主動實作 clone()