JavaScript 常用设计模式详解
Category(分类): JavaScript Status: 已整理(2026)
原文件只有原文链接,正文缺失。本文根据原文《Javascript 常用的设计模式详解》补回工厂、单例、模块、代理、职责链、命令、模板方法、策略、发布-订阅和中介者十个主题,并清理旧页面抓取代码。原文中的 IIFE、构造函数和 DOM 示例作为历史背景保留,同时补充 ES module、组合、函数和事件机制的现代写法。
一、先理解“模式”
设计模式不是必须照抄的类模板,而是对反复出现的设计问题和权衡的命名。使用模式前应先问:
- 是否真的存在重复的变化点?
- 是否能通过普通函数、模块、组合或数据结构更简单地解决?
- 额外的间接层是否会提高可测试性和可替换性?
- 对象生命周期、错误处理和资源清理是否清晰?
GoF《设计模式》通常把 23 个经典模式分为:创建型 5 个、结构型 7 个、行为型 11 个。本文只整理原文涉及的十种 JavaScript 常见实践,不代表设计模式的完整目录。
二、工厂模式(Factory)
工厂把对象的创建与使用分开。JavaScript 中常见的三种形式应区分:
- 简单工厂:根据参数选择并返回具体对象;
- 工厂方法:父类定义创建流程,具体创建延迟给子类覆写;
- 抽象工厂:创建一组相互关联的产品。
2.1 简单工厂
原文的 CreatePerson 可以改写为普通工厂函数:
function createPerson(name, age, sex) {
return {
name,
age,
sex,
sayName() {
return this.name
}
}
}
const person1 = createPerson('longen', 28, '男')
const person2 = createPerson('tugenhua', 27, '女')
console.log(person1.sayName()) // longen
console.log(typeof person1) // object
console.log(person1 instanceof Object) // true
这种方式的优点是简单、无需 new,也能根据输入返回不同结构;缺点是返回的对象没有专门的构造器身份,实例方法若写在工厂内部则会重复创建。
如果需要类型身份和共享方法,可以使用 class 或显式原型:
class Person {
constructor(name, age, sex) {
this.name = name
this.age = age
this.sex = sex
}
sayName() {
return this.name
}
}
function createPersonByType(type, data) {
if (type === 'person') return new Person(data.name, data.age, data.sex)
throw new TypeError(`未知类型:${type}`)
}
const person = createPersonByType('person', {
name: 'Ada',
age: 36,
sex: 'unknown'
})
console.log(person instanceof Person) // true
2.2 工厂方法
原文的自行车商店示例意在说明:购买流程放在父类,具体自行车由子类创建。下面使用 class 表达同样的结构:
class BicycleShop {
sellBicycle(model) {
const bicycle = this.createBicycle(model)
bicycle.check()
bicycle.assemble()
return bicycle
}
createBicycle() {
throw new Error('子类必须实现 createBicycle()')
}
}
class LongenBicycleShop extends BicycleShop {
createBicycle(model) {
return {
model,
check() {
console.log('检查自行车')
},
assemble() {
console.log(`组装 ${this.model}`)
}
}
}
}
const shop = new LongenBicycleShop()
const bicycle = shop.sellBicycle('city')
console.log(bicycle.model) // city
这才是更接近 GoF “工厂方法”的结构。仅使用 switch 的一个函数通常应称为简单工厂,而不是工厂方法。

图片来源:原文博客图片,已下载到本地。
2.3 工厂的取舍
优点:
- 集中处理创建逻辑;
- 使用方依赖抽象能力,而不必知道具体构造器;
- 新增产品时可以减少调用方改动。
缺点:
- 工厂自身会成为新的间接层;
- 产品类型很多时,条件分支可能变得庞大;
- 如果创建过程本来就简单,直接使用对象字面量或构造器更清楚。
三、单体/单例模式(Singleton)
原文使用“单体模式”这一中文名称,常与 GoF Singleton、对象字面量和模块模式混用。应区分:
- 对象字面量可以是命名空间,但并不意味着它具有“只能实例化一次”的构造语义;
- GoF Singleton 试图把某个类的实例数量限制为一个,并提供访问入口;
- ES module 的顶层状态通常在同一个模块记录/loader 上下文中只初始化一次,但不同 Worker、iframe、Realm 或 Node loader 可能各有一份;
- 很多时候,模块导出的单个对象或依赖注入容器已经足够,不需要额外的 Singleton 类。
3.1 历史构造函数写法
function Singleton(name) {
this.name = name
}
Singleton.prototype.getName = function () {
return this.name
}
const getInstance = (() => {
let instance
return name => {
if (!instance) instance = new Singleton(name)
return instance
}
})()
const first = getInstance('aa')
const second = getInstance('bb')
console.log(first === second) // true
console.log(second.getName()) // aa:第一次创建的参数生效
原文的 this.instance 公开挂在构造函数或实例上,容易被外部修改。闭包可以隐藏实例引用,但它仍然是可变的全局入口,测试时需要提供重置或依赖注入方案。
3.2 现代模块状态
ES module 通常可以直接导出模块内创建的对象:
// logger.js
const logs = []
export function log(message) {
logs.push({ message, time: Date.now() })
}
export function getLogs() {
return logs.slice()
}
// app.js
import { getLogs, log } from './logger.js'
log('started')
console.log(getLogs())
这个模块在同一模块图中通常只评估一次,但不能据此宣称整个浏览器或所有 Realm 只有一个 logger。不同 iframe、Worker、动态模块 URL 和 Node loader 都可能拥有不同的模块状态。
3.3 单例的风险
- 全局共享状态会降低测试隔离性;
- 初始化顺序和隐式依赖不容易发现;
Object.freeze()只进行浅冻结,不会自动冻结嵌套数组或对象;- “只有一个实例”经常被误认为“所有环境都只有一个实例”。
如果只是需要共享配置,优先使用显式传入的依赖:
function createUserService({ logger, repository }) {
return {
save(user) {
logger.log(`保存 ${user.name}`)
return repository.save(user)
}
}
}
四、模块模式(Module Pattern)
4.1 IIFE 模块模式
ES module 普及前,IIFE 常用于创建私有变量和公开 API:
const legacyModule = (() => {
let privateNumber = 112
function privateFunction() {
return privateNumber
}
return {
increment() {
privateNumber += 1
},
getValue() {
return privateFunction()
}
}
})()
legacyModule.increment()
console.log(legacyModule.getValue()) // 113
IIFE 的私有状态来自闭包。它仍可用于维护旧项目,但现代项目优先使用 ES module 的模块作用域:
// counter.js
let value = 0
export function increment() {
value += 1
}
export function getValue() {
return value
}
模块模式的优点是减少全局变量并隐藏实现;缺点是导出的对象仍可能暴露可变引用,API 设计必须明确哪些值允许外部修改。
4.2 增强模块模式
原文用“先创建某种类型实例,再为实例增加公开方法”的方式实现增强模块:
class CustomType {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
const application = (() => {
const privateValue = 'aa'
const object = new CustomType('tugenhua')
object.label = 'aa'
object.getPrivateValue = () => privateValue
return object
})()
console.log(application instanceof CustomType) // true
console.log(application.getName()) // tugenhua
console.log(application.getPrivateValue()) // aa

图片来源:原文博客图片,已下载到本地。
如果不需要兼容旧浏览器,直接用 ES module、class 私有字段或闭包函数通常更清晰。
五、代理模式(Proxy Pattern)
设计模式中的代理为真实对象提供一个间接访问入口,代理和真实对象通常遵守相同的接口。常见目的包括:
- 访问控制;
- 延迟初始化(虚拟代理);
- 远程代理;
- 缓存代理;
- 日志、限流和权限检查。
设计模式中的“代理”与 ECMAScript Proxy 对象有关联但不等价:前者是设计关系,后者是语言提供的元编程 API。
5.1 延迟初始化
class ExpensiveImage {
constructor(url) {
this.url = url
console.log('创建昂贵资源')
}
render() {
return `render: ${this.url}`
}
}
class ImageProxy {
#realImage
constructor(url) {
this.url = url
}
render() {
if (!this.#realImage) {
this.#realImage = new ExpensiveImage(this.url)
}
return this.#realImage.render()
}
}
const image = new ImageProxy('/large-image.png')
console.log('此时还没有创建真实图片')
console.log(image.render())
5.2 浏览器图片预加载
原文的图片代理示例使用了已经失效的 HTTP loading 图片和旧图片地址。下面示例假设运行在浏览器中,页面存在 <img id="preview">,调用方应传入项目中真实存在的占位图和目标图片 URL;示例只展示代理职责,不承诺这些示例路径对应本仓库资源。
function createImageView(imageElement) {
return {
setSrc(src) {
imageElement.src = src
}
}
}
function createPreloadImageProxy(view, placeholder) {
let requestId = 0
return {
setSrc(src) {
const currentRequest = ++requestId
view.setSrc(placeholder)
const image = new Image()
image.onload = () => {
if (currentRequest === requestId) view.setSrc(src)
}
image.onerror = () => {
if (currentRequest === requestId) view.setSrc(placeholder)
}
image.src = src
},
cancel() {
requestId += 1
}
}
}
const imageElement = document.querySelector('#preview')
if (!imageElement) throw new Error('页面需要 <img id="preview">')
const view = createImageView(imageElement)
const proxy = createPreloadImageProxy(view, '/assets/loading.svg')
// 使用项目中真实存在的图片 URL:
// proxy.setSrc('/assets/photo.webp')
5.3 缓存代理
原文用对象和参数拼接字符串作为缓存键,可能发生键冲突,也不能自然处理对象参数。可以根据参数类型选择 Map,并明确缓存策略:
function memoizeUnary(fn) {
const cache = new Map()
return function memoized(value) {
if (cache.has(value)) return cache.get(value)
const result = fn.call(this, value)
cache.set(value, result)
return result
}
}
const square = memoizeUnary(value => value * value)
console.log(square(4)) // 16
console.log(square(4)) // 16:第二次命中缓存
// 该简单缓存假定函数是纯函数,且结果只由 value 决定;
// 如果函数依赖 this,应把接收者纳入缓存键或不要使用这个封装。
多参数缓存需要嵌套 Map、稳定序列化或显式缓存键;不能随意用 JSON.stringify 作为通用方案,因为属性顺序、循环引用、函数和特殊对象都会带来问题。缓存还要考虑上限、过期和清理,否则缓存本身可能成为内存保留点。
六、职责链模式(Chain of Responsibility)
职责链把请求依次交给多个处理节点,每个节点可以处理请求,也可以交给下一个节点。原文的抽奖案例可以用返回值明确表达“已处理/继续”:
class Chain {
constructor(handler) {
this.handler = handler
this.next = null
}
setNext(next) {
this.next = next
return next
}
handle(request) {
const result = this.handler(request)
if (result !== Chain.NEXT) return result
return this.next ? this.next.handle(request) : undefined
}
static NEXT = Symbol('next')
}
const order500 = new Chain(request => {
if (request.orderType === 1 && request.isPay) {
return '中奖 100 元红包'
}
return Chain.NEXT
})
const order200 = new Chain(request => {
if (request.orderType === 2 && request.isPay) {
return '中奖 20 元红包'
}
return Chain.NEXT
})
const normal = new Chain(request =>
request.count > 0 ? '获得 10 元优惠券' : '请再接再厉'
)
order500.setNext(order200).setNext(normal)
console.log(order500.handle({ orderType: 2, isPay: true, count: 0 }))
使用 Symbol 而不是字符串 "nextSuccessor",可以避免业务返回值与控制信号冲突。链节点之间只依赖统一的处理协议,因此可以插入、删除或重新排序。
6.1 异步职责链
异步处理不能依赖同步返回值来表示“继续”,可以让每个节点返回 Promise,或者显式调用 next:
function createAsyncChain(handlers) {
return async function run(request) {
let index = 0
async function next() {
const handler = handlers[index++]
if (!handler) return undefined
return handler(request, next)
}
return next()
}
}
const run = createAsyncChain([
async (request, next) => {
if (request.type === 'cache') return '缓存命中'
return next()
},
async (request, next) => {
await Promise.resolve()
if (request.type === 'network') return '网络处理'
return next()
}
])
run({ type: 'network' }).then(console.log)
真实项目还要处理超时、取消、异常传播和“没有节点处理”的结果。
七、命令模式(Command)
命令模式把请求封装成对象或函数,让调用者不必知道接收者的具体实现。它适用于菜单、按钮、队列、撤销/重做和任务调度。
7.1 对象形式
class MenuBar {
refresh() {
console.log('刷新菜单目录')
}
}
class SubMenu {
add() {
console.log('增加子菜单')
}
remove() {
console.log('删除子菜单')
}
}
class RefreshMenuCommand {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.refresh()
}
}
function setCommand(button, command) {
button.addEventListener('click', () => command.execute())
}
7.2 JavaScript 中的函数命令
由于函数本身是一等值,简单命令不必创建很多只有 execute() 方法的类:
function setFunctionCommand(button, command) {
button.addEventListener('click', command)
}
const command = () => console.log('执行命令')
如果要支持撤销,可以显式定义 execute 和 undo:
function createCommand(execute, undo = () => {}) {
return { execute, undo }
}
const command = createCommand(
() => console.log('添加'),
() => console.log('撤销添加')
)
八、模板方法模式(Template Method)
模板方法在父类中固定算法骨架,把可变步骤交给子类。JavaScript 也可以使用组合函数代替继承:
class Game {
start() {
this.setup()
this.play()
this.finish()
}
setup() {}
play() {}
finish() {}
}
class Chess extends Game {
setup() {
console.log('准备国际象棋')
}
play() {
console.log('开始下棋')
}
finish() {
console.log('结束棋局')
}
}
new Chess().start()
在 JavaScript 中,模板方法的抽象基类不一定需要 abstract 关键字,也可以用函数接收步骤:
function runGame({ setup, play, finish }) {
setup()
play()
finish()
}
runGame({
setup: () => console.log('准备游戏'),
play: () => console.log('进行游戏'),
finish: () => console.log('结束游戏')
})
九、策略模式(Strategy)
策略模式把可替换算法封装起来。JavaScript 中策略经常就是函数表:
const discountStrategies = {
normal(price) {
return price
},
vip(price) {
return price * 0.9
},
employee(price) {
return price * 0.8
}
}
function calculatePrice(price, strategyName) {
const strategy = discountStrategies[strategyName]
if (typeof strategy !== 'function') {
throw new RangeError('未知折扣策略')
}
return strategy(price)
}
console.log(calculatePrice(100, 'vip')) // 90
这比为每个策略创建一层 class 更轻。策略函数应尽量保持输入、输出和副作用契约一致;如果策略需要状态,可以使用对象或 class。
十、发布-订阅与观察者
原文将发布-订阅称为观察者模式的一个例子。两者有关联但不完全相同:
- 观察者:主题对象直接持有观察者并通知它们;
- 发布-订阅:发布者和订阅者通常通过事件总线/主题名解耦;
- 中介者:多个组件通过一个协调对象交互,不只负责广播事件。
10.1 观察者
class Subject {
#observers = new Set()
subscribe(observer) {
this.#observers.add(observer)
return () => this.#observers.delete(observer)
}
notify(value) {
for (const observer of this.#observers) {
observer(value)
}
}
}
const subject = new Subject()
const unsubscribe = subject.subscribe(value => {
console.log('收到更新:', value)
})
subject.notify('new state')
unsubscribe()
使用 Set 可以避免重复订阅;返回取消函数可以明确生命周期。
10.2 发布-订阅总线
class EventBus {
#events = new Map()
on(type, listener) {
let listeners = this.#events.get(type)
if (!listeners) {
listeners = new Set()
this.#events.set(type, listeners)
}
listeners.add(listener)
return () => this.off(type, listener)
}
off(type, listener) {
const listeners = this.#events.get(type)
if (!listeners) return
listeners.delete(listener)
if (listeners.size === 0) this.#events.delete(type)
}
emit(type, payload) {
for (const listener of this.#events.get(type) ?? []) {
listener(payload)
}
}
}
const bus = new EventBus()
const stop = bus.on('user:created', user => console.log(user.name))
bus.emit('user:created', { name: 'Ada' })
stop()
如果使用 DOM EventTarget,可以直接借助标准事件机制和 AbortController 管理批量取消:
const controller = new AbortController()
const target = new EventTarget()
target.addEventListener('change', event => {
console.log(event.detail.value)
}, { signal: controller.signal })
target.dispatchEvent(new CustomEvent('change', {
detail: { value: 'updated' }
}))
controller.abort()
不要让事件总线无边界地保存监听器,否则会造成内存和调试问题;事件名称、错误传播和订阅取消都应有约定。
十一、中介者模式(Mediator)
中介者集中协调多个对象,减少组件之间的网状依赖。它与发布-订阅的区别在于,中介者通常包含更明确的业务编排逻辑:
class ChatMediator {
#users = new Set()
register(user) {
this.#users.add(user)
user.mediator = this
}
send(sender, message) {
for (const user of this.#users) {
if (user !== sender) user.receive(message, sender)
}
}
}
class ChatUser {
constructor(name) {
this.name = name
this.mediator = null
}
send(message) {
this.mediator?.send(this, message)
}
receive(message, sender) {
console.log(`${sender.name} -> ${this.name}: ${message}`)
}
}
const mediator = new ChatMediator()
const ada = new ChatUser('Ada')
const grace = new ChatUser('Grace')
mediator.register(ada)
mediator.register(grace)
ada.send('Hello')
中介者并不是万能的“上帝对象”。如果所有业务规则都被塞进一个中介者,它会变成难以维护的巨型类;应按领域边界拆分协调职责。
十二、模式选择建议
| 问题 | 可以考虑 | 现代 JavaScript 优先选项 |
|---|---|---|
| 创建逻辑复杂或类型可替换 | 工厂 | 工厂函数、class、依赖注入 |
| 需要共享模块状态 | 单例/模块 | ES module,避免隐式全局状态 |
| 需要隐藏私有实现 | 模块 | ES module、闭包、私有字段 |
| 需要延迟/控制访问 | 代理 | 函数包装、Proxy、显式接口 |
| 多个处理者依次尝试 | 职责链 | 数组 + next、中间件 |
| 将操作排队、撤销或记录 | 命令 | 函数命令或 { execute, undo } |
| 算法可替换 | 策略 | 函数表、函数参数 |
| 状态变化通知多个对象 | 观察者/发布订阅 | EventTarget、事件总线、响应式库 |
| 多组件需要协调 | 中介者 | 领域服务或明确的协调器 |
设计模式的价值在于降低变化成本,而不是让代码看起来更“面向对象”。如果一层普通函数、Map、模块导出或组合就能表达意图,就不必强行引入复杂的类层次。