你不需要成為語言學家,才有資格把 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設計模式 : Flyweight(享元模式)
javaScript設計模式 : Chain of Responsibility(責任鏈)
javaScript設計模式 : Command Pattern(命令模式)
javaScript設計模式 : Interpreter(直譯器)
javaScript設計模式 : Iterator(迭代器)
javaScript設計模式 : Mediator(仲裁者)
javaScript設計模式 : Observer( 觀察者 )
javaScript設計模式 : Strategy(策略模式)
