javaScript設計模式 : Prototype(原型)

 


你不需要成為語言學家,才有資格把 prototype 用好。把它想成「把共同邏輯放在上層,實例拿不到就往上找」;再把 Prototype 設計模式理解成「先養一個優良母體,後面都靠複製省功」。

本文用四段路線帶你走:先拆清 JS 原型鏈、class 與 Ctor.prototype 的真面目;再示範 Object.create 與 structuredClone 的取捨;接著做一個可切換策略的「原型工廠」,讓委派式與深拷貝能按場景切換;最後把常見雷點一次列好,附上修正範式。

沒有硬背定義、沒有花俏口號,只有能貼進專案的片段與原則:方法共享省記憶體、資料需要獨立就拷、不要在熱路徑改原型、別動內建原型。花一小段時間,換來更清晰的結構與更少的 bug,這筆帳通常划算。希望本篇文章可以幫助到需要的您。


目錄

{tocify} $title={目錄} 


快速導讀

prototype(小寫):

        JS 物件委派與方法共享的語言機制;每個物件都有 [[Prototype]] 指向另一個物件。

Prototype(大寫)設計模式:

        用「複製(clone)既有樣本」產生新物件,而不是每次都 new 從零建構。

兩者在 JS 會相遇:用 Object.create(proto) 以某個「樣本」做原型,然後再做差異化,可同時拿到「設計模式的複製」與「語言機制的委派」兩種好處。


兩個「Prototype」:機制 vs. 設計模式

1.    JS 的原型機制(prototypal inheritance)

    每個物件都內含 [[Prototype]](俗稱 __proto__,不建議直接用),組成原型鏈。

    讀取屬性時,先找自身,沒有就沿著原型鏈往上找。

    方法共享:把方法放在原型上,一份記憶體、N 個實例共用。


2.    Prototype 設計模式(GoF)

    用一個既有實例當樣本,透過複製快速生成新實例。

    適合成本高(建構昂貴)、結構複雜、或高度客製的物件建立。

在 JS 中,兩者可疊加:以某個樣本為原型(Object.create)+ 覆寫差異,比傳統 new + 一堆參數更直覺。


JavaScript 原型機制速查

1.    四個容易混淆的名詞

obj.__proto__:

        物件的 [[Prototype]] 非標準屬性(現已被 Web 兼容保留),不建議在生產使用。

Object.getPrototypeOf(obj) / Object.setPrototypeOf(obj, proto):

        正規 API,但 setPrototypeOf 會影響效能,不要在熱路徑頻繁呼叫。

Ctor.prototype:

        函式的原型物件,只有搭配 new Ctor() 生成實例時會用到。

class:

        ES6 語法糖,底層仍是 prototype。


2.    原型鏈與屬性遮蔽(shadowing)

const proto = { kind: 'base', run() { console.log('run from proto'); } };
const obj = Object.create(proto);
obj.kind = 'child';
obj.run(); // run from proto(方法向上委派)
console.log(obj.kind); // 'child'(自身屬性遮蔽原型同名屬性)


3.    用 Object.create 取代「空建構子」

const RectangleProto = {
  area() { return this.w * this.h; },
  describe() { return `${this.name} ${this.w}x${this.h}`; }
};

function makeRect(w, h, name = 'Rect') {
  const r = Object.create(RectangleProto);
  r.w = w; r.h = h; r.name = name;
  return r;
}

const a = makeRect(10, 20);
console.log(a.area()); // 200


4.    class 與 prototype 的對應

class User {
  constructor(name) { this.name = name; }
  greet() { return `Hi, ${this.name}`; } // 放在 User.prototype
}
const u = new User('Ada');
console.log(Object.getPrototypeOf(u) === User.prototype); // true


Prototype 設計模式在 JavaScript 的實作

1.    為什麼用「複製」而不是 new?

    建構昂貴:需要外部資源、耗時初始化。

    高度客製:多種組態,參數組合複雜。

    一致性:從合法樣本複製,比重新建構更不易遺漏設定。


2.    三種常見複製策略

    2.1    淺拷貝(shallow clone):Object.assign({}, src) 或展開運算子 {...src}。

             快、簡單;巢狀物件仍是同一參考。

    2.2    深拷貝(deep clone):structuredClone(src)(現代瀏覽器/Node 17+)

             支援 Date/Map/Set/ArrayBuffer 等;不複製函式。

    2.3    原型委派式複製:Object.create(src)

             新物件把 src 設為原型;不是複製資料,是委派查找。適合做「樣板 + 局部覆寫」。


在 JS 中,設計模式的「原型」= 用一個可作為樣板的物件。實作可用 Object.create(委派式)、或「拷貝」策略(真正複製資料),依需求選擇。


3.    原型註冊表(Prototype Registry)

// 以名稱註冊可被 clone 的樣板
const Registry = new Map();

const EnemyProto = {
  type: 'enemy',
  hp: 100,
  atk: 10,
  attack(target) { target.hp -= this.atk; }
};

const BossProto = Object.create(EnemyProto);
BossProto.type = 'boss';
BossProto.hp = 500;
BossProto.atk = 50;

Registry.set('enemy.basic', EnemyProto);
Registry.set('enemy.boss', BossProto);

// 委派式 clone(Object.create)
function spawn(name, overrides = {}) {
  const proto = Registry.get(name);
  if (!proto) throw new Error(`Unknown prototype: ${name}`);
  const obj = Object.create(proto);
  return Object.assign(obj, overrides);
}

const e1 = spawn('enemy.basic', { hp: 120 });
const b1 = spawn('enemy.boss', { atk: 60 });


4.    深拷貝型 clone(保留資料獨立)

function deepCloneUsingStructuredClone(src) {
  return structuredClone(src); // Node 18+/現代瀏覽器
}

// 若要保留方法,可搭配原型 + 資料深拷貝
function cloneWithProtoAndData(protoObj) {
  const clone = Object.create(Object.getPrototypeOf(protoObj));
  return Object.assign(clone, structuredClone(protoObj));
}


實戰案例

1.    UI 設定樣板:用原型委派做差異化

const BaseTheme = {
  font: 'Inter, sans-serif',
  radius: 12,
  spacing: 8,
  palette: { primary: '#3467eb', text: '#222' },
  buttonStyle() {
    return `padding:${this.spacing}px ${this.spacing*2}px; border-radius:${this.radius}px; color:${this.palette.text}`;
  }
};

const DarkTheme = Object.create(BaseTheme);
DarkTheme.palette = Object.assign(Object.create(BaseTheme.palette), { text: '#eee' });
// ↑ palette 也用原型委派,避免整個覆蓋

const CozyTheme = Object.create(BaseTheme);
CozyTheme.spacing = 12;
CozyTheme.radius = 16;

const btnCss = CozyTheme.buttonStyle(); // 使用 CozyTheme 的 spacing/radius


2.    表單「範本 -> 具體表單」:設計模式的 clone

const FormTemplate = {
  title: '聯絡我們',
  fields: [{ name: 'email', type: 'email', required: true }],
  submitText: '送出',
  validate(data) { return this.fields.every(f => !f.required || data[f.name]); }
};

function cloneForm(tpl, overrides) {
  // 深拷貝 fields,避免共享陣列/物件
  const base = structuredClone(tpl);
  return Object.assign(base, overrides);
}

const JobForm = cloneForm(FormTemplate, {
  title: '職缺應徵',
  fields: structuredClone(FormTemplate.fields).concat([{ name: 'resume', type: 'file', required: true }])
});


3.    遊戲單位:原型註冊 + 稍作客製

const Unit = {
  name: 'Unit',
  hp: 100, atk: 10, def: 5, spd: 10,
  dmg(target) { target.hp -= Math.max(0, this.atk - target.def); }
};

const Archer = Object.create(Unit);
Archer.name = 'Archer'; Archer.atk = 14; Archer.spd = 12;

const Knight = Object.create(Unit);
Knight.name = 'Knight'; Knight.atk = 12; Knight.def = 10;

const Registry2 = new Map([['archer', Archer], ['knight', Knight]]);
const spawn2 = (key, x = {}) => Object.assign(Object.create(Registry2.get(key)), x);

const a = spawn2('archer', { hp: 110 });
const k = spawn2('knight');
a.dmg(k);


記憶體與效能:方法放哪裡、clone 用哪種?

方法放原型:

        function Ctor(){} 然後 Ctor.prototype.method = ... 或 class 的實例方法 —— 一份方法,多個實例共享。

每個實例都新建箭頭函式:

        this.method = () => {} 會每次配置新函式,記憶體成本更高,僅在需要捕捉當前 this 或閉包變數時使用。

Object.create vs 深拷貝:

        委派式(Object.create)快、記憶體省;但資料是共享的(對巢狀物件尤其要注意)。真正需要資料獨立時才 deep clone。

避免頻繁 Object.setPrototypeOf:

        更改已建立物件的原型會破壞 JIT 假設,慢。原型應在建立時就定好。


與工廠/建造者/抽象工廠的關係

Factory Method / Abstract Factory:封裝「如何生產」;輸入參數產出物件。

Builder:逐步組裝複雜物件。

Prototype:透過「複製既有樣本」來產生新物件,不是從零建。

實務上可搭配:抽象工廠內部透過原型註冊表選樣本,再 clone 出去;Builder 完成一次昂貴建構後,把結果註冊為原型,往後直接複製。


API 與語法補充

// 取得/設定原型(注意:setPrototypeOf 影響效能)
const p = Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, newProto);

// 建立物件並指定原型(推薦)
const o = Object.create(proto, {
  name: { value: 'Ada', enumerable: true, writable: true }
});

// 判斷自有屬性 vs 原型屬性
obj.hasOwnProperty('x'); // true = 自有
'x' in obj;              // true = 物件或其原型鏈上有此屬性
Object.keys(obj);        // 僅列出自有且可列舉


常見錯誤與雷點

1. 忘記 new 造成 this 指向錯誤

function User(name){ this.name = name; }
const u = User('Ada'); // ❌ this 指向 global/undefined(嚴格模式)
// 正確:
const u2 = new User('Ada'); // ✅


避免法:在建構式內加保護:

function User(name){
  if (!(this instanceof User)) return new User(name);
  this.name = name;
}


2. 把原型方法寫成箭頭函式

function Counter(){ this.n = 0; }
Counter.prototype.inc = () => { this.n++; }; // ❌ `this` 固定為外層(非實例)
// 正確:
Counter.prototype.inc = function(){ this.n++; }; // ✅


3. 在熱路徑呼叫 Object.setPrototypeOf / 操作 __proto__

會拖慢 JIT 最佳化。在建立時就用 Object.create 設定好原型。


4. 深淺拷貝分不清

const a = { nest: { x: 1 } };
const b = { ...a }; // 淺拷貝
b.nest.x = 2;
console.log(a.nest.x); // 2(被影響)❌
// 需要資料獨立請用 structuredClone 或遞迴深拷貝


5.    用 JSON.stringify/parse 當深拷貝

會遺失 Date/Map/Set/RegExp/函式/循環參考,數值精度也可能有風險。只在「單純資料」時權衡使用。


6.    枚舉時把原型鏈也掃進來

for (const k in obj) { /* 也會列出原型的可列舉屬性 */ } // 需搭配 hasOwnProperty
Object.keys(obj); // 只列自有可列舉屬性(較安全)


7.    整個覆蓋 Ctor.prototype = {...} 損失 constructor

function A(){}
A.prototype = { x: 1 }; // ❌ A.prototype.constructor 不再指向 A
// 修正:
A.prototype = Object.assign(Object.create(Object.prototype), { 
  constructor: A, x: 1 
});


8. 修改內建原型(污染全域)

String.prototype.toKebab = function(){ /* … */ }; // ❌ 高風險
// 用工具函式或自家 util 模組取代


9.    class 欄位初始化覆蓋原型方法

class X {
  method() { /* prototype method */ }
  method = 1; // ❌ 實例欄位同名,遮蔽了原型上的 method
}


10.    以為 Object.create(src) 是「資料拷貝」

它只是設原型,巢狀物件仍會共享參考;需要資料獨立請再深拷貝或以覆蓋策略處理。


實用範式

A. 用樣板 + 差異化(委派式)

function derive(proto, patch) {
  return Object.assign(Object.create(proto), patch);
}
const Base = { a(){}, b(){} };
const X = derive(Base, { role: 'x' });


B. 註冊-複製(設計模式版)

const reg = new Map();
const tpl = { /* ... */ };
reg.set('tpl', tpl);

const clone = (key, overrides = {}) => Object.assign(structuredClone(reg.get(key)), overrides);


C. 安全枚舉

for (const k of Object.keys(obj)) { /* 自有可列舉 */ }


D. 判斷來源

obj.hasOwnProperty('x'); // 自有屬性?
'x' in obj;              // 自有或原型鏈上?


逐步打造「原型模式工廠」

以下整合一個小型、可用於產品設定/遊戲資料/表單範本的「原型工廠」:

// 1) 可選:為原型加上 clone 方法(策略可切換)
const CloneStrategy = {
  delegate(proto, overrides = {}) {
    return Object.assign(Object.create(proto), overrides);
  },
  deepCopy(proto, overrides = {}) {
    const base = structuredClone(proto);
    // 保留原型鏈(可選):把 base 掛回同原型,以保留方法委派
    const o = Object.create(Object.getPrototypeOf(proto));
    return Object.assign(o, base, overrides);
  }
};

// 2) 建立註冊表
class PrototypeFactory {
  constructor(strategy = CloneStrategy.delegate) {
    this.strategy = strategy;
    this.store = new Map();
  }
  register(name, protoObj) { this.store.set(name, protoObj); }
  make(name, overrides = {}) {
    const p = this.store.get(name);
    if (!p) throw new Error(`Unknown prototype: ${name}`);
    return this.strategy(p, overrides);
  }
}

//// 使用示例
const Product = {
  sku: 'GENERIC',
  price: 100,
  tags: [],
  describe() { return `${this.sku} $${this.price}`; }
};

const Premium = Object.create(Product);
Premium.sku = 'PREMIUM';
Premium.price = 299;
Premium.tags = ['vip'];

// 委派式(省記憶體)
const pf = new PrototypeFactory(CloneStrategy.delegate);
pf.register('basic', Product);
pf.register('prem', Premium);

const p1 = pf.make('basic', { sku: 'B-01', price: 120 });
const p2 = pf.make('prem', { tags: Object.assign(Object.create(Premium.tags), { 1: 'gift' }) }); // 或改為深拷貝策略

// 切換策略(需要資料隔離)
const pf2 = new PrototypeFactory(CloneStrategy.deepCopy);
pf2.register('prem', Premium);
const p3 = pf2.make('prem', { price: 279 });


問題集

Q1:class 取代 prototype 了嗎?

A:沒有。class 是語法糖;實例方法仍放在 Class.prototype。

Q2:為何避免直接用 __proto__?

A:相容性遺留屬性;在效能與可預期性上劣於 Object.create / getPrototypeOf。

Q3:什麼時候要深拷貝?

A:需要資料互不影響、包含巢狀物件時。否則優先用委派式(省記憶體、快)。

Q4:可不可以改內建原型?

A:除非你完全掌握環境且能承擔風險,不要。請用工具函式或 polyfill 隔離。


結語

JavaScript 的 prototype 機制(方法共享、屬性委派)與 Prototype 設計模式(以樣本複製)在這門語言裡是天作之合:Object.create 做骨架、structuredClone 或 Object.assign 做資料差異化,即可在效能與彈性之間拿到漂亮的平衡。把「樣板」變成可重用資產,外加註冊表與可替換的 clone 策略,你就擁有一套清晰、可維護、可擴充的物件生成體系。


延伸閱讀推薦:

javaScript設計模式 : Factory Method   (工廠方法)

javaScript設計模式 : Abstract Factory(抽象工廠)

javaScript設計模式 : Builder(建造者模式)

javaScript設計模式 : Singleton (單例模式)

javaScript設計模式 : Adapter(轉接器模式)

javaScript設計模式 : Bridge( 橋接模式 )

javaScript設計模式 : Composite(組合模式)

 javaScript設計模式 : Decorator(裝飾者)

javaScript設計模式 : Facade(外觀模式)

javaScript設計模式 : Flyweight(享元模式)

javaScript設計模式 : Proxy(代理模式)

javaScript設計模式 : Chain of Responsibility(責任鏈)

javaScript設計模式 : Command Pattern(命令模式)

javaScript設計模式 : Interpreter(直譯器)

javaScript設計模式 : Iterator(迭代器)

javaScript設計模式 : Mediator(仲裁者)

javaScript設計模式 : Memento(備忘錄)

javaScript設計模式 : Observer( 觀察者 )

javaScript設計模式 : State(狀態模式)

javaScript設計模式 : Strategy(策略模式)

javaScript設計模式 : Template Method (模板方法)

javaScript設計模式 : Visitor(訪問者模式)

張貼留言 (0)
較新的 較舊