JavaScript 设计模式:让你的代码更容易变化
Category(分类): JavaScript Status: 已整理(2026)
原文标题带有夸张的宣传语,正文包含 GoF 23 种模式的示例。本文保留原文的分类、模式名称、意图和示例主线,修复页面代码说明、复制控件标记、注释与代码粘连、错误的原型模式和罗马数字解释器,并补充 JavaScript 中更常见的函数、模块、组合、事件和
Reflect写法。
一、设计模式不是越多越好
经典 GoF 模式按目的分为三类:
创建型模式(5 个)
- 工厂方法(Factory Method)
- 抽象工厂(Abstract Factory)
- 单例(Singleton)
- 建造者(Builder)
- 原型(Prototype)
结构型模式(7 个)
- 适配器(Adapter)
- 装饰器(Decorator)
- 代理(Proxy)
- 外观(Facade)
- 桥接(Bridge)
- 组合(Composite)
- 享元(Flyweight)
行为型模式(11 个)
- 策略(Strategy)
- 模板方法(Template Method)
- 观察者(Observer)
- 迭代器(Iterator)
- 责任链(Chain of Responsibility)
- 命令(Command)
- 备忘录(Memento)
- 状态(State)
- 访问者(Visitor)
- 中介者(Mediator)
- 解释器(Interpreter)
JavaScript 是多范式语言。函数、闭包、对象组合和 ES module 往往比完整的类层次更简单;不要为了“使用设计模式”而制造不必要的抽象。
二、创建型模式
2.1 工厂方法模式
工厂方法的关键是:父类定义创建流程,子类决定具体创建哪一种产品。原文的 AnimalFactory 用 switch 直接选择产品,更准确地说是简单工厂。下面给出更接近工厂方法的例子:
class Animal {
speak() {
throw new Error('子类必须实现 speak()')
}
}
class Dog extends Animal {
speak() {
return 'Woof!'
}
}
class Cat extends Animal {
speak() {
return 'Meow!'
}
}
class AnimalStore {
orderAnimal() {
const animal = this.createAnimal()
return animal.speak()
}
createAnimal() {
throw new Error('子类必须实现 createAnimal()')
}
}
class DogStore extends AnimalStore {
createAnimal() {
return new Dog()
}
}
class CatStore extends AnimalStore {
createAnimal() {
return new Cat()
}
}
console.log(new DogStore().orderAnimal()) // Woof!
console.log(new CatStore().orderAnimal()) // Meow!
如果需求只是根据类型选择构造器,简单工厂通常更直接:
const animalFactories = {
dog: () => new Dog(),
cat: () => new Cat()
}
function createAnimal(type) {
const factory = animalFactories[type]
if (!factory) throw new RangeError(`未知动物类型:${type}`)
return factory()
}
2.2 抽象工厂模式
抽象工厂创建的是一组相互匹配的产品。下面用“主题下的按钮和输入框”说明:
class LightButton {
render() {
return '<button class="light">Light</button>'
}
}
class LightInput {
render() {
return '<input class="light">'
}
}
class DarkButton {
render() {
return '<button class="dark">Dark</button>'
}
}
class DarkInput {
render() {
return '<input class="dark">'
}
}
class LightUiFactory {
createButton() {
return new LightButton()
}
createInput() {
return new LightInput()
}
}
class DarkUiFactory {
createButton() {
return new DarkButton()
}
createInput() {
return new DarkInput()
}
}
function renderForm(factory) {
return `${factory.createButton().render()}${factory.createInput().render()}`
}
console.log(renderForm(new DarkUiFactory()))
抽象工厂适合产品族必须保持一致的场景;如果产品之间没有一致性约束,多个简单工厂或普通函数可能更容易维护。
2.3 单例模式
原文的 Logger.instance 作为公开属性并不安全,且 Object.freeze(logger) 只能浅冻结。一个更小的模块级例子是:
const logger = (() => {
const logs = []
return Object.freeze({
log(message) {
logs.push(message)
console.log(`Logger: ${message}`)
},
getCount() {
return logs.length
},
getLogs() {
return logs.slice()
}
})
})()
logger.log('First message')
console.log(logger.getCount()) // 1
这个对象在当前模块实例中是共享的,但“单例”边界取决于模块图、Realm、iframe、Worker 和 Node loader。需要测试隔离时,优先导出工厂函数,或将 logger 作为依赖传给业务对象。
2.4 建造者模式
建造者通过多个步骤创建复杂对象。原文的三明治示例可以保留,但应避免 builder 意外复用旧状态:
class Sandwich {
constructor(ingredients) {
this.ingredients = Object.freeze([...ingredients])
}
toString() {
return this.ingredients.join(', ')
}
}
class SandwichBuilder {
#ingredients = []
addMeat(meat) {
this.#ingredients.push(meat)
return this
}
addCheese(cheese) {
this.#ingredients.push(cheese)
return this
}
addVegetables(...vegetables) {
this.#ingredients.push(...vegetables)
return this
}
build() {
const sandwich = new Sandwich(this.#ingredients)
this.#ingredients = []
return sandwich
}
}
const sandwich = new SandwichBuilder()
.addMeat('ribeye steak')
.addCheese('american cheese')
.addVegetables('peppers', 'onions')
.build()
console.log(String(sandwich)) // ribeye steak, american cheese, peppers, onions
链式 builder 适合可选参数很多、构建过程需要校验的对象。若对象只有两三个字段,对象字面量或参数对象更清晰。
2.5 原型模式
原型模式与 JavaScript 的原型机制相关,但下面这句需要纠正:
const carPrototype = {
wheels: 4,
color: 'red',
start() {
return 'Starting the car'
}
}
const car = Object.create(carPrototype)
car.color = 'blue'
console.log(car.wheels) // 4:从原型继承
console.log(car.color) // blue:自有属性覆盖原型属性
console.log(Object.getPrototypeOf(car) === carPrototype) // true
Object.create(carPrototype) 不是浅拷贝,而是创建一个新对象并设置其 [[Prototype]]。原型中的对象属性仍然会被共享:
const prototype = { settings: { color: 'red' } }
const first = Object.create(prototype)
const second = Object.create(prototype)
first.settings.color = 'blue'
console.log(second.settings.color) // blue:共享嵌套对象
如果需要浅复制,应使用展开或 Object.assign;如果需要结构化数据复制,可以根据类型使用 structuredClone,但函数、DOM 节点和许多带内部状态的对象不能任意克隆。
三、结构型模式
3.1 适配器模式
适配器把一个接口转换成客户端期待的接口:
class LegacyPayment {
payInCents(cents) {
return `legacy paid ${cents} cents`
}
}
class PaymentAdapter {
constructor(legacyPayment) {
this.legacyPayment = legacyPayment
}
pay(amount) {
if (!Number.isFinite(amount) || amount < 0) {
throw new RangeError('金额必须是非负有限数字')
}
return this.legacyPayment.payInCents(Math.round(amount * 100))
}
}
const payment = new PaymentAdapter(new LegacyPayment())
console.log(payment.pay(12.5)) // legacy paid 1250 cents
适配器的职责是转换协议,不应偷偷改变业务语义。参数单位、错误类型和异步行为都应在适配器中明确。
3.2 装饰器模式
设计模式中的装饰器通过包装对象动态增加行为:
class BasicCoffee {
cost() {
return 10
}
description() {
return '咖啡'
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee
}
cost() {
return this.coffee.cost() + 2
}
description() {
return `${this.coffee.description()} + 牛奶`
}
}
const coffee = new MilkDecorator(new BasicCoffee())
console.log(coffee.description(), coffee.cost()) // 咖啡 + 牛奶 12
JavaScript 中函数装饰也很常见:
function withLogging(fn, logger = console) {
return function decorated(...args) {
logger.log('调用开始', args)
const result = Reflect.apply(fn, this, args)
logger.log('调用结束', result)
return result
}
}
上面的运行时包装与 ECMAScript @decorator 语法不是一回事。TC39 装饰器提案的阶段会变化,不能把 TypeScript legacy decorators、Babel 插件语法或提案语法称为已普遍支持的原生 ES7 语法。
3.3 代理模式
代理控制对真实主题的访问:
class RealDocument {
read() {
return 'document content'
}
}
class DocumentProxy {
constructor(document, user) {
this.document = document
this.user = user
}
read() {
if (this.user?.canRead !== true) {
throw new Error('没有读取权限')
}
return this.document.read()
}
}
const proxy = new DocumentProxy(new RealDocument(), { canRead: true })
console.log(proxy.read())
ECMAScript Proxy 可以拦截属性访问,但必须遵守代理不变式,并不自动等于设计模式中的代理:
const target = { value: 1 }
const observed = new Proxy(target, {
get(object, property, receiver) {
console.log('读取', String(property))
return Reflect.get(object, property, receiver)
}
})
console.log(observed.value) // 1
3.4 外观模式
外观为复杂子系统提供更小的入口:
class Inventory {
reserve(productId) {
return `reserved ${productId}`
}
}
class Payment {
charge(userId, amount) {
return `charged ${userId}: ${amount}`
}
}
class Shipping {
ship(productId, address) {
return `shipping ${productId} to ${address}`
}
}
class OrderFacade {
constructor() {
this.inventory = new Inventory()
this.payment = new Payment()
this.shipping = new Shipping()
}
placeOrder({ userId, productId, amount, address }) {
this.inventory.reserve(productId)
this.payment.charge(userId, amount)
return this.shipping.ship(productId, address)
}
}
console.log(new OrderFacade().placeOrder({
userId: 'u1',
productId: 'p1',
amount: 100,
address: 'Shanghai'
}))
外观应隐藏不必要的复杂度,但不应吞掉所有错误或让调用者无法获得关键结果。
3.5 桥接模式
桥接把抽象和实现拆开,使两者可以独立变化:
class ConsoleRenderer {
render(message) {
return `[console] ${message}`
}
}
class JsonRenderer {
render(message) {
return JSON.stringify({ message })
}
}
class Notification {
constructor(renderer) {
this.renderer = renderer
}
send(message) {
return this.renderer.render(message)
}
}
console.log(new Notification(new JsonRenderer()).send('hello'))
当两个维度都需要独立扩展时,桥接可以避免组合爆炸;如果只有一个维度,直接组合往往足够。
3.6 组合模式
组合用树形结构统一处理叶子和容器:
class FileNode {
constructor(name) {
this.name = name
}
size() {
return 1
}
}
class DirectoryNode {
constructor(name) {
this.name = name
this.children = []
}
add(child) {
this.children.push(child)
return this
}
size() {
return this.children.reduce((total, child) => total + child.size(), 0)
}
}
const root = new DirectoryNode('root')
root.add(new FileNode('a.txt'))
root.add(new DirectoryNode('src').add(new FileNode('app.js')).add(new FileNode('utils.js')))
console.log(root.size()) // 3
组合的共同接口应尽量稳定;不要为了形式统一让叶子实现没有意义的 add() 或 remove()。
3.7 享元模式
享元共享大量对象中的不变内部状态,把变化的外部状态作为参数传入:
class CharacterStyle {
#font
#color
constructor(font, color) {
this.#font = font
this.#color = color
}
render(char, position) {
return { char, position, font: this.#font, color: this.#color }
}
}
class StyleFactory {
#styles = new Map()
get(font, color) {
const key = `${font}\u0000${color}`
if (!this.#styles.has(key)) {
this.#styles.set(key, new CharacterStyle(font, color))
}
return this.#styles.get(key)
}
}
const styles = new StyleFactory()
const red = styles.get('serif', 'red')
console.log(red === styles.get('serif', 'red')) // true
console.log(red.render('A', 0))
享元会引入缓存和生命周期管理成本,只有在共享状态确实很多时才有收益。
四、行为型模式
4.1 策略模式
JavaScript 中策略经常就是函数:
const strategies = {
normal: price => price,
vip: price => price * 0.9,
employee: price => price * 0.8
}
function finalPrice(price, strategyName) {
const strategy = strategies[strategyName]
if (typeof strategy !== 'function') throw new RangeError('未知策略')
return strategy(price)
}
console.log(finalPrice(100, 'vip')) // 90
相比原文中为 StrategyA、StrategyB 创建 class,函数表减少了样板代码;当策略需要自己的状态或生命周期时,再使用对象或 class。
4.2 模板方法模式
class Game {
start() {
this.setup()
this.play()
this.finish()
}
setup() {}
play() {}
finish() {}
}
class Chess extends Game {
setup() { console.log('Setting up chess') }
play() { console.log('Playing chess') }
finish() { console.log('Finishing chess') }
}
class TicTacToe extends Game {
setup() { console.log('Setting up TicTacToe') }
play() { console.log('Playing TicTacToe') }
finish() { console.log('Finishing TicTacToe') }
}
new Chess().start()
new TicTacToe().start()
模板方法适合算法骨架稳定、步骤实现由子类决定的情况;如果步骤只是少量函数,组合函数更轻量。
4.3 观察者模式
观察者是主题直接持有的一组订阅者:
class Subject {
#observers = new Set()
attach(observer) {
this.#observers.add(observer)
return () => this.#observers.delete(observer)
}
notify(value) {
for (const observer of this.#observers) observer.update(value)
}
}
class ConcreteObserver {
update(value) {
console.log('收到状态:', value)
}
}
const subject = new Subject()
const remove = subject.attach(new ConcreteObserver())
subject.notify('new state')
remove()
实际项目中要明确通知顺序、异常策略、重复订阅和取消生命周期。
4.4 迭代器模式
原文的 has_next()/next() 是一种手写迭代器,但不符合 JavaScript 标准迭代协议。现代写法实现 [Symbol.iterator]():
class Collection {
constructor(items = []) {
this.items = [...items]
}
*[Symbol.iterator]() {
yield* this.items
}
}
const collection = new Collection(['item 1', 'item 2', 'item 3'])
for (const item of collection) {
console.log(item)
}
const iterator = collection[Symbol.iterator]()
console.log(iterator.next()) // { value: 'item 1', done: false }
标准迭代器的 next() 返回 { value, done }。异步数据应实现 [Symbol.asyncIterator](),配合 for await...of。
4.5 责任链模式
class Handler {
constructor() {
this.nextHandler = null
}
setNext(handler) {
this.nextHandler = handler
return handler
}
handle(request) {
return this.nextHandler?.handle(request)
}
}
class HandlerA extends Handler {
handle(request) {
return request === 'A' ? 'Handle A' : super.handle(request)
}
}
class HandlerB extends Handler {
handle(request) {
return request === 'B' ? 'Handle B' : super.handle(request)
}
}
const handlerA = new HandlerA()
handlerA.setNext(new HandlerB())
console.log(handlerA.handle('B')) // Handle B
console.log(handlerA.handle('C')) // undefined
如果节点要异步处理,应返回 Promise 或显式传递 next,不要把一个同步字符串和异步回调混在同一协议中。
4.6 命令模式
class Receiver {
actionA() { return 'Receiver Action A' }
actionB() { return 'Receiver Action B' }
}
class Command {
constructor(receiver, action) {
this.receiver = receiver
this.action = action
}
execute() {
return this.receiver[this.action]()
}
}
class Invoker {
#commands = new Map()
setCommand(key, command) {
this.#commands.set(key, command)
}
executeCommand(key) {
return this.#commands.get(key)?.execute()
}
}
const receiver = new Receiver()
const invoker = new Invoker()
invoker.setCommand('A', new Command(receiver, 'actionA'))
console.log(invoker.executeCommand('A')) // Receiver Action A
命令对象可以保存参数、执行时间、撤销函数和重做信息。简单按钮事件则直接使用回调即可。
4.7 备忘录模式
备忘录保存发起人的状态快照,并由管理者保存历史:
class Memento {
#state
constructor(state) {
this.#state = Object.freeze(structuredClone(state))
}
getState() {
return structuredClone(this.#state)
}
}
class Editor {
constructor(text = '') {
this.text = text
}
setText(text) {
this.text = text
}
save() {
return new Memento({ text: this.text })
}
restore(memento) {
this.text = memento.getState().text
}
}
const editor = new Editor('State A')
const snapshot = editor.save()
editor.setText('State B')
editor.restore(snapshot)
console.log(editor.text) // State A
structuredClone 只适合可结构化克隆的数据;对于函数、DOM 节点、数据库连接等状态,应定义专门的快照格式,而不是盲目复制。
4.8 状态模式
状态模式把不同状态下的行为拆开:
class Context {
constructor() {
this.state = new StateA(this)
}
setState(state) {
this.state = state
}
request() {
return this.state.handle()
}
}
class StateA {
constructor(context) { this.context = context }
handle() {
this.context.setState(new StateB(this.context))
return 'Handle State A'
}
}
class StateB {
constructor(context) { this.context = context }
handle() {
this.context.setState(new StateA(this.context))
return 'Handle State B'
}
}
const context = new Context()
console.log(context.request()) // Handle State A
console.log(context.request()) // Handle State B
状态模式适合状态转换复杂、每个状态有多个行为的场景。只有一两个条件分支时,直接使用 if 或状态表通常更清楚。
4.9 访问者模式
访问者把针对不同元素的操作集中在访问者对象中,代价是新增元素类型需要修改访问者接口:
class Circle {
constructor(radius) {
this.radius = radius
}
accept(visitor) {
return visitor.visitCircle(this)
}
}
class Rectangle {
constructor(width, height) {
this.width = width
this.height = height
}
accept(visitor) {
return visitor.visitRectangle(this)
}
}
class AreaVisitor {
visitCircle(circle) {
return Math.PI * circle.radius * circle.radius
}
visitRectangle(rectangle) {
return rectangle.width * rectangle.height
}
}
const visitor = new AreaVisitor()
console.log(new Circle(1).accept(visitor)) // 3.141592653589793
console.log(new Rectangle(3, 4).accept(visitor)) // 12
访问者适合元素类型稳定、操作经常增加的场景;如果类型和操作都经常变化,普通函数分派或模式匹配式数据结构可能更灵活。
4.10 中介者模式
中介者让多个组件通过一个协调对象通信:
class Mediator {
#components = new Set()
register(component) {
component.mediator = this
this.#components.add(component)
}
notify(sender, event) {
for (const component of this.#components) {
if (component !== sender) component.receive(sender, event)
}
}
}
class Component {
constructor(name) {
this.name = name
this.mediator = null
}
send(event) {
this.mediator?.notify(this, event)
}
receive(sender, event) {
console.log(`${sender.name} -> ${this.name}: ${event}`)
}
}
const mediator = new Mediator()
const componentA = new Component('A')
const componentB = new Component('B')
mediator.register(componentA)
mediator.register(componentB)
componentA.send('Hello')
如果中介者积累了太多业务条件,就会变成新的巨型对象,应按领域拆分协调逻辑。
4.11 解释器模式
解释器把语法规则表示为对象并逐步解释。原文罗马数字示例的条件顺序错误:必须先匹配 CM/CD、XC/XL、IX/IV 等双字符规则,再匹配单字符规则。
class RomanContext {
constructor(input) {
this.input = input
this.output = 0
}
}
class Thousands {
interpret(context) {
while (context.input.startsWith('M')) {
context.output += 1000
context.input = context.input.slice(1)
}
}
}
class Hundreds {
interpret(context) {
const rules = [
['CM', 900],
['CD', 400],
['D', 500],
['C', 100]
]
for (const [token, value] of rules) {
if (context.input.startsWith(token)) {
context.output += value
context.input = context.input.slice(token.length)
return
}
}
}
}
class Tens {
interpret(context) {
const rules = [
['XC', 90],
['XL', 40],
['L', 50],
['X', 10]
]
for (const [token, value] of rules) {
if (context.input.startsWith(token)) {
context.output += value
context.input = context.input.slice(token.length)
return
}
}
}
}
class Ones {
interpret(context) {
const rules = [
['IX', 9],
['IV', 4],
['V', 5],
['I', 1]
]
for (const [token, value] of rules) {
if (context.input.startsWith(token)) {
context.output += value
context.input = context.input.slice(token.length)
return
}
}
}
}
class RomanInterpreter {
static parse(input) {
const context = new RomanContext(input)
for (const expression of [
new Thousands(),
new Hundreds(),
new Tens(),
new Ones()
]) {
// 每个单位最多需要处理多次,例如 VIII 或 III。
if (expression instanceof Thousands) {
expression.interpret(context)
} else {
while (context.input.length > 0) {
const before = context.input
expression.interpret(context)
if (before === context.input) break
}
}
}
if (context.input !== '') {
throw new SyntaxError(`无法解析的罗马数字:${context.input}`)
}
return context.output
}
}
console.log(RomanInterpreter.parse('CDXLVIII')) // 448
这个示例按单位顺序解释常见组合,但没有实现完整的罗马数字正规语法校验,例如重复次数和非法减法组合;生产代码应使用经过验证的解析函数或状态表。解释器模式的重点是把语法规则拆成可组合对象。
五、常见误区与现代替代
5.1 把类包装器称作原生装饰器
class Decorator extends Component 是运行时设计模式。它不等于 @logged 这样的 ECMAScript 装饰器语法。TypeScript legacy decorators、Babel 插件和 TC39 提案必须分别说明编译目标和标准阶段。
5.2 把 Object.create 当作 clone
Object.create(proto) 建立原型关系,不复制原型中的属性。浅拷贝使用展开或 Object.assign;结构化数据复制可以考虑 structuredClone,但没有通用的“复制任意 JavaScript 对象” API。
5.3 把发布-订阅、观察者和中介者混为一谈
它们都可以传递事件,但耦合关系不同。文章设计时应说明谁持有谁、谁决定通知对象,以及订阅如何取消。
5.4 为每个策略创建 class
函数是 JavaScript 的一等值,简单算法策略用函数通常更直观。只有当策略需要独立状态、继承契约或生命周期时,class 才更有价值。
5.5 设计模式替代领域建模
模式解决的是结构重复和变化方向,不会替代数据建模、错误处理、权限校验、事务和测试。优先让依赖显式、接口小、资源可取消。