Constructor Pattern:用模具生出同款物件
此為《Learning JavaScript Design Patterns, 2nd Edition》學習筆記,對應第 7 章(JavaScript Design Patterns - The Constructor Pattern)
模式用途
- 分類:建立型
- 解決什麼問題:如何建立多個結構一致、可共享方法的物件
從按鈕開始
為了更貼近實際開發情境,我打算用一個最常見的元件——按鈕——當作案例。登入、登出、註冊⋯⋯這些按鈕雖然長得差不多,但都需要 id、文字標籤 (label)、點擊事件 (onClick)。
從這個案例出發,我會先展示最原始的寫法,再一步步演進到 Constructor Pattern。
一步步演進
1. 用大括號直接定義物件
最直接的方式就是先寫一個物件,然後手動複製多個:
let btn1 = {
id: 'login',
label: '登入',
onClick: () => console.log('登入')
}
let btn2 = {
id: 'logout',
label: '登出',
onClick: () => console.log('登出')
}
這樣當然可以用,不過每次都要重複定義 onClick
,寫多了就不容易維護。這種做法一開始很快,但當需要更多按鈕時就會顯得笨重。
補充:object literal這種直接用大括號
{}
來建立物件,專有名詞叫做 object literal(物件字面值)1。
2. 用函式幫忙產生物件
手動寫每個物件太重複了,所以我乾脆把重複邏輯包成一個函式,讓它在最後 return
一個物件。這樣每次呼叫函式就能拿到一個新的物件,程式碼會簡潔很多。
function createButton(id, label) {
return {
id,
label,
onClick() {
console.log(`${label} clicked`)
}
}
}
let btn1 = createButton('login', '登入')
let btn2 = createButton('logout', '登出')
這樣少打很多字,但問題還在:每次呼叫 createButton
,都會複製一份新的 onClick
方法。十顆按鈕就十份,這樣會浪費掉不少記憶體,而且如果要修改方法,就得改很多份,不容易維護。
補充:factory function這種「用函式幫忙產生物件」的寫法,在 JavaScript 社群常被叫作 factory function(工廠函式)。
3. 用 new
自動化建立物件
用函式 return
物件雖然方便,但還是有兩個缺點:一是方法會一直被複製,二是我得自己寫 return
。能不能更自動一點?
這就是 new
2 的用處。當我在一個函式前面加上 new
,JavaScript 會幫我做以下幾件事:
- 建立一個新物件,並把它的內部
[[Prototype]]
指到該函式的prototype
。 - 以此新物件作為
this
執行函式本體。 - 如果函式裡有額外用
return
回傳一個物件,new
最後會拿那個物件當結果;如果沒有這樣做,new
就會自動把步驟 (1) 建立的新物件當作結果回傳。
function Button(id, label) {
this.id = id
this.label = label
}
let btn1 = new Button('login', '登入')
let btn2 = new Button('logout', '登出')
以上方的例子來說,new Button('login', '登入')
就會是:
- 建立一個新的空物件
{}
。 - 把這個新物件的原型(
[[Prototype]]
)指向Button.prototype
。 - 以這個新物件作為
this
執行Button
函式:this.id = id
會把"login"
設定到新物件的id
屬性。this.label = label
會把"登入"
設定到新物件的label
屬性。
- 最後,這個新物件(已經有
id
和label
屬性)會被回傳並指派給btn1
。
這樣生出來的 btn1、btn2 就是這個函式的 instance (實例)。
Constructor function 的命名與限制
Button(...)
這類設計來搭配new
使用的函式,通常被稱作 constructor function(建構函式),慣例上會用大寫開頭。注意箭頭函式與物件字面值都不能用new
。
如果沒有 new,會發生什麼事?如果直接呼叫
Button(...)
而不是用new Button(...)
:
- 在非嚴格模式下,
this
指到全域物件,會污染全域。- 在嚴格模式下,
this
是undefined
,會直接報錯。
4. prototype
:共享方法
方法則放在 prototype
3 上,這樣所有 instance 就能共用:
function Button(id, label) {
this.id = id
this.label = label
}
// 共享的方法要放在 prototype 上
Button.prototype.onClick = function () {
console.log(`${this.label} clicked`)
}
let btn1 = new Button('login', '登入')
let btn2 = new Button('logout', '登出')
// 驗證方法是否真的共享
console.log(btn1.onClick === btn2.onClick) // true 同一個方法!
btn1.onClick() // 登入 clicked
btn2.onClick() // 登出 clicked
不論有幾顆按鈕,方法都只需要寫一份。
prototype 三角關係
prototype
:只有函式才有的屬性。它指向一個物件,這個物件的目的就是存放要給 instance (實例) 共用的屬性與方法。[[Prototype]]
:每個物件都有的內部屬性。它指向該物件的「原型」。當你用new Button()
建立btn1
時,btn1
的[[Prototype]]
就會指向Button.prototype
。__proto__
4:是為了相容性而保留的存取器屬性(legacy accessor),建議使用Object.getPrototypeOf()
方法來讀取。
5. Constructor Pattern

這種做法有幾個好處:
- 可以很有系統地生出很多長得一樣、功能也一致的物件,不用一直重複寫
- 按鈕們都共用
prototype
上的方法,所以不會每顆按鈕都複製一份,省記憶體又好維護 - 因為有
prototype
這條線連著,可以用instanceof
5 來判斷「這是不是 Button 做出來的」,也可以像做「IconButton」這種加上圖示的新按鈕,在原本 Button 的基礎上擴充功能,做出更多變化的元件
簡單來說,Constructor Pattern 就是讓我可以做出一堆「同款」物件,大家都能用同一組方法,也方便之後加新功能或做型別判斷。
6. ES6 class
語法糖
後來 ES6 推出了 class
6,讓寫法更直觀:
class Button {
constructor(id, label) {
this.id = id
this.label = label
}
onClick() {
console.log(`${this.label} clicked`)
}
}
let btn1 = new Button('login', '登入')
let btn2 = new Button('logout', '登出')
屬性和方法都能寫在同一個區塊,看起來就是一個「完整物件的定義」,不必再分散在 constructor 與 prototype
。不過要注意,它其實只是語法糖,底層依然是 constructor function 搭配 prototype
在運作。
重點整理:五步學習卡
1) 問題來源
建立多個相似物件時:
- 物件字面值:重複又難維護
- 用函式幫忙產生物件:方法一份份複製
👉 需要一致的建立方式,還能共享方法。
2) 白話描述

Constructor Pattern 就像「按鈕模具」:
- 倒入不同的材料(id, label)
- 出來一顆顆按鈕(instance)
- 它們共享同一份「做法」(
prototype
上的方法)
3) 專業術語
在 JavaScript 裡,Constructor Pattern = constructor function + prototype
。這是一種物件建立模式,提供一致的方式來產生物件,並透過 prototype
共享方法與支援繼承。
4) 視覺化

在畫這張圖時,我參考了 Just JavaScript 這套互動式教材的圖示語彙。我很喜歡它用「變數就像一條線,指向某個值」的比喻,搭配視覺化的方式,讓我這個偏向視覺學習的人更能掌握抽象概念。這次延伸到物件之間的關聯,我也沿用了相同的表達方式。
5) 程式碼與適用情境
適用情境
- 需要建立大量結構一致、方法應該共享的情況
- 需要
instanceof
型別判斷與繼承
不適用情境
- 只需要少量物件、一次性的物件(物件字面值通常最簡單)
- 方法不需要共享或後續擴充/繼承(函式就足夠)
ES5 Constructor Pattern
function Button(id, label) {
this.id = id
this.label = label
}
Button.prototype.onClick = function () {
console.log(`${this.label} clicked`)
}
let btn1 = new Button('login', '登入')
let btn2 = new Button('logout', '登出')
這樣生出來的 btn1
、btn2
,就是用 Button 這個「模具」打造出來的物件。這種透過 new
建立的物件,通常會被叫做它的 instance。
ES6 Class 語法糖
class Button {
constructor(id, label) {
this.id = id
this.label = label
}
onClick() {
console.log(`${this.label} clicked`)
}
}
const btn1 = new Button('login', '登入')
const btn2 = new Button('logout', '登出')
ES6
class
只是 Constructor Pattern 的語法糖,本質與 ES5 constructor function +prototype
相同。兩者都是同一個模式的不同表達方式。
後記
原本以為 Constructor Pattern 很單純,結果一路寫下來才發現裡頭其實藏了不少細節。new
的流程、prototype
的關聯被一個個拉出來,才發現自己有很多地方不熟,只好邊寫邊補,等於又把之前沒完全搞懂的東西重新整理了一遍。
而且也讓我意識到,只有「五步學習卡片」是不太夠的。最後我調整了筆記的結構:開頭先放「模式用途」,交代它屬於哪一類模式、要解的問題是什麼,幫自己抓住焦點;範例換成前端常見的按鈕,並在程式碼前加一段情境鋪墊,好更快進入狀況;最後再用五步卡片收斂重點,把過程和整理拆開,讀起來更清楚。
這些調整其實都是邊寫邊試出來的,一直在想怎麼安排才有循序漸進的感覺。過程中也做了不少取捨:有些地方先放進來,有些則決定暫時拿掉,這個拿捏的過程還蠻有趣的。
把零散的觀念連起來後,整個脈絡都清楚了,這種把知識點打通的感覺真的很爽快呀!😆