ข้ามไปยังเนื้อหา

Prototype

Prototype สร้าง object ใหม่ด้วยการคัดลอก instance ที่มีอยู่และตั้งค่าไว้ครบถ้วนแล้ว ซึ่งก็คือ prototype แทนที่จะสร้างขึ้นมาใหม่ตั้งแต่ต้น

บางครั้งการสร้าง object จาก constructor ของตัวเองก็แพงหรือยุ่งยาก อาจต้องใช้การคำนวณที่แพง การตั้งค่าจำนวนมาก หรือความรู้ที่ผู้เรียกไม่มี ถ้าคุณถือ instance ที่เกือบจะเป็นอย่างที่คุณต้องการอยู่แล้ว การคัดลอก instance นั้นแล้วปรับแต่งสำเนาก็ง่ายกว่าการสร้างใหม่ Prototype ทำให้ “ขอตัวหนึ่งที่เหมือนตัวนี้” กลายเป็น first-class operation: ตัว object รู้วิธี clone ตัวเอง

การตัดสินใจที่นิยาม Prototype คือ shallow เทียบกับ deep shallow copy ทำสำเนา object ระดับบนสุด แต่แชร์ reference ที่ซ้อนอยู่ภายใน ดังนั้นการแก้ field ที่ซ้อนอยู่ในสำเนาก็จะเปลี่ยนต้นฉบับด้วย ส่วน deep copy ทำสำเนาทั้งกราฟ ทำให้สำเนาเป็นอิสระโดยสมบูรณ์ การเลือกผิดเป็นต้นเหตุคลาสสิกของบั๊ก “สำเนา” ที่แอบแชร์ list กับต้นฉบับจะสร้างความประหลาดใจให้คุณในครั้งแรกที่ฝ่ายใดฝ่ายหนึ่งถูกแก้ Prototype บังคับให้คุณเลือกสิ่งนั้นอย่างตั้งใจ

classDiagram
  class Prototype {
    <<interface>>
    +clone() Prototype
  }
  class Document {
    +title: string
    +tags: List
    +clone() Document
  }
  class Client {
    +duplicate(p) Prototype
  }
  Prototype <|.. Document
  Client ..> Prototype : clones
prototype เปิดเผยการดำเนินการ clone ที่ client เรียกใช้
  • Prototype — interface ที่ประกาศการดำเนินการ clone
  • Concrete Prototype (Document) — implement clone เพื่อผลิตสำเนาของตัวเอง โดยตัดสินใจว่าจะ shallow หรือ deep
  • Client — ได้ object ใหม่ด้วยการ clone prototype แทนที่จะเรียก constructor

Document ที่มี list ของ tags เรา clone แบบ deep เพื่อให้การแก้ไข tags ของสำเนาไม่กระทบต้นฉบับ

class Document {
constructor(
public title: string,
public tags: string[],
) {}
// Deep clone: structuredClone copies the nested array too.
clone(): Document {
const copy = structuredClone({ title: this.title, tags: this.tags });
return new Document(copy.title, copy.tags);
}
}
const original = new Document('Report', ['draft', 'q2']);
const copy = original.clone();
copy.title = 'Report (revised)';
copy.tags.push('final');
console.log(original.title, original.tags); // Report ['draft', 'q2']
console.log(copy.title, copy.tags); // Report (revised) ['draft', 'q2', 'final']
  • ข้อดี: ถูกกว่าการสร้างใหม่เมื่อ object สร้างได้แพงหรือซับซ้อน
  • ข้อดี: client คัดลอก object ที่มีอยู่โดยไม่ต้องพึ่งพา concrete class หรืออาร์กิวเมนต์ของ constructor ของตัวเอง
  • ข้อดี: ให้คุณเก็บคลัง prototype ที่ตั้งค่าไว้ล่วงหน้าและปั๊มสร้างรูปแบบต่าง ๆ ออกมา
  • ข้อเสีย: การ clone กราฟ object ที่มี cycle หรือทรัพยากรที่แชร์กัน (ไฟล์ที่เปิดอยู่, socket) เป็นเรื่องยุ่งยาก
  • ข้อเสีย: ความแตกต่างระหว่าง shallow กับ deep ผิดพลาดได้ง่าย ทำให้เกิดสำเนาที่แอบแชร์ state กัน
  • Builder สร้าง object ทีละขั้นตอน ส่วน Prototype เริ่มจากตัวที่ทำสำเร็จไว้แล้ว
  • Abstract Factory สามารถเก็บและคืน prototype แทนที่จะสร้าง product แต่ละตัวขึ้นใหม่
PrototypeBuilderFactory Method
วิธีสร้างcopy จากต้นแบบที่มีอยู่สร้างใหม่ทีละขั้นสร้างจาก subclass
เหมาะกับobject ที่ตั้งค่า costly หรือซับซ้อนobject ที่มี optional config เยอะdefer การเลือก type ไปให้ subclass
ต้องระวังdeep copy vs shallow copyลำดับ step ต้องถูกต้องsubclass proliferation
ตัวอย่างใน frameworkObject.assign(), spread, clone()URLSearchParams, query builderConnection.create(), Logger.get()

💡 หมายเหตุสำหรับ developer

Pattern นี้พบได้บ่อยใน:

  • JavaScript structuredClone() / object spread — clone object ที่มีอยู่แล้วแทนสร้างใหม่ทั้งหมด
  • Redux state updates — reducer สร้าง state ใหม่ด้วยการ clone state เดิมแล้วแก้บางส่วน (immutability ผ่าน prototype-style copy)
  • Java Cloneable interface — บังคับให้ class ประกาศชัดว่ารองรับการ clone ตัวเอง
Prototype สร้าง object ใหม่อย่างไร?
ความแตกต่างระหว่าง shallow copy กับ deep copy คืออะไร?
ทำไม shallow copy จึงก่อให้เกิดบั๊กได้?