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

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 常用设计模式详解

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/35-Javascript设计模式详解
本文目录13 个章节
  1. 一、先理解“模式”
  2. 二、工厂模式(Factory)
  3. 三、单体/单例模式(Singleton)
  4. 四、模块模式(Module Pattern)
  5. 五、代理模式(Proxy Pattern)
  6. 六、职责链模式(Chain of Responsibility)
  7. 七、命令模式(Command)
  8. 八、模板方法模式(Template Method)
  9. 九、策略模式(Strategy)
  10. 十、发布-订阅与观察者
  11. 十一、中介者模式(Mediator)
  12. 十二、模式选择建议
  13. 参考资料

JavaScript 常用设计模式详解

Category(分类): JavaScript Status: 已整理(2026)

原文件只有原文链接,正文缺失。本文根据原文《Javascript 常用的设计模式详解》补回工厂、单例、模块、代理、职责链、命令、模板方法、策略、发布-订阅和中介者十个主题,并清理旧页面抓取代码。原文中的 IIFE、构造函数和 DOM 示例作为历史背景保留,同时补充 ES module、组合、函数和事件机制的现代写法。

原文:Javascript 常用的设计模式详解

一、先理解“模式”

设计模式不是必须照抄的类模板,而是对反复出现的设计问题和权衡的命名。使用模式前应先问:

  • 是否真的存在重复的变化点?
  • 是否能通过普通函数、模块、组合或数据结构更简单地解决?
  • 额外的间接层是否会提高可测试性和可替换性?
  • 对象生命周期、错误处理和资源清理是否清晰?

GoF《设计模式》通常把 23 个经典模式分为:创建型 5 个、结构型 7 个、行为型 11 个。本文只整理原文涉及的十种 JavaScript 常见实践,不代表设计模式的完整目录。

二、工厂模式(Factory)

工厂把对象的创建与使用分开。JavaScript 中常见的三种形式应区分:

  1. 简单工厂:根据参数选择并返回具体对象;
  2. 工厂方法:父类定义创建流程,具体创建延迟给子类覆写;
  3. 抽象工厂:创建一组相互关联的产品。

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('执行命令')

如果要支持撤销,可以显式定义 executeundo

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、模块导出或组合就能表达意图,就不必强行引入复杂的类层次。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS