技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 设计模式:让你的代码更容易变化

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/36-JavaScript设计模式:让你的代码像个天才!
本文目录6 个章节
  1. 一、设计模式不是越多越好
  2. 二、创建型模式
  3. 三、结构型模式
  4. 四、行为型模式
  5. 五、常见误区与现代替代
  6. 参考资料

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 工厂方法模式

工厂方法的关键是:父类定义创建流程,子类决定具体创建哪一种产品。原文的 AnimalFactoryswitch 直接选择产品,更准确地说是简单工厂。下面给出更接近工厂方法的例子:

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

相比原文中为 StrategyAStrategyB 创建 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/CDXC/XLIX/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 设计模式替代领域建模

模式解决的是结构重复和变化方向,不会替代数据建模、错误处理、权限校验、事务和测试。优先让依赖显式、接口小、资源可取消。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS