设计模式详解(生活类比 + 原生 JavaScript 示例)
目录
使用说明
- 每个模式都用同样的结构:
- 一句话定义
- 生活类比(贴近日常生活)
- 解决什么问题
- 什么时候用
- 原生 JS 示例代码
- 建议阅读顺序:单例 → 工厂 → 策略 → 观察者/发布订阅 → 代理 → 外观 → 其他。
创建型模式
1. 单例模式 (Singleton)
一句话定义
保证一个类在整个程序中只有一个实例,并提供一个全局访问点。
生活类比
- 整个城市只有一个「110 报警中心」,大家打电话都是连到同一个地方。
- 公司只有一个「财务系统」,所有部门都往这一个系统里录数据。
解决什么问题
- 多个地方各自创建同一种对象,导致数据不统一、状态不同步。
- 配置/日志等“全局信息”需要集中管理。
什么时候用
- 全局配置中心:环境变量、接口地址等。
- 全局日志器:统一打印日志。
- 全局缓存/存储:简单的 in-memory 缓存。
JS 示例:配置中心单例
class Config {
constructor() {
// 如果已经创建过一次,直接返回同一个实例
if (Config.instance) {
return Config.instance
}
// 第一次创建时初始化默认配置
this.env = 'prod'
this.apiHost = 'https://api.example.com'
Config.instance = this
}
setEnv(env) {
this.env = env
}
}
const c1 = new Config()
const c2 = new Config()
console.log(c1 === c2) // true,总是同一个对象
console.log(c1.env, c2.env) // prod prod
c1.setEnv('dev')
console.log(c2.env) // dev,c2 也跟着变
2. 工厂模式 (Factory)
一句话定义
不在外面自己 new,而是通过一个“工厂函数/类”来创建对象,由工厂决定创建哪种具体对象。
生活类比
- 你点外卖:只选「汉堡套餐 / 披萨套餐」,后厨怎么组合主食+饮料+小食,你不用管。
- 买手机套餐:「学生套餐 / 商务套餐」,具体包含的服务由运营商决定。
解决什么问题
- 对象创建逻辑复杂、到处复制粘贴。
- 需要根据不同条件创建不同“类型”的对象。
什么时候用
- 有一类东西“长得差不多但略有不同”,例如不同类型的用户、不同类型的通知、不同支付渠道对象等。
JS 示例:用户工厂
function createUser(role) {
if (role === 'admin') {
return { role, permissions: ['read', 'write', 'delete'] }
}
if (role === 'editor') {
return { role, permissions: ['read', 'write'] }
}
if (role === 'guest') {
return { role, permissions: ['read'] }
}
// 默认
return { role: 'guest', permissions: ['read'] }
}
const admin = createUser('admin')
const guest = createUser('guest')
3. 建造者模式 (Builder)
一句话定义
把一个复杂对象的创建过程拆成很多小步骤,通过链式调用一步一步“组装”出来。
生活类比
- 点奶茶:先选杯型 → 选甜度 → 选冰量 → 选加料 → 确认下单。
- 买车:选车系 → 选颜色 → 选轮毂 → 选内饰 → 选配置包。
解决什么问题
- 构造函数参数太多,
new Something(a, b, c, d, e, f)可读性极差。 - 需要可选配置很多、但每次用到的只是一部分。
什么时候用
- 配置类对象(请求配置、表单配置、图表配置等)。
- 需要链式调用构建数据结构的场景。
JS 示例:奶茶建造者
class DrinkBuilder {
constructor() {
this.drink = {
type: '奶茶',
size: '中杯',
sugar: '正常糖',
ice: '正常冰',
toppings: []
}
}
setSize(size) {
this.drink.size = size
return this
}
noSugar() {
this.drink.sugar = '无糖'
return this
}
lessIce() {
this.drink.ice = '少冰'
return this
}
addTopping(topping) {
this.drink.toppings.push(topping)
return this
}
build() {
return this.drink
}
}
const order = new DrinkBuilder()
.setSize('大杯')
.noSugar()
.lessIce()
.addTopping('珍珠')
.addTopping('椰果')
.build()
console.log(order)
结构型模式
4. 适配器模式 (Adapter)
一句话定义
写一层“转换器”,把一个接口/数据的格式,转换成另一种需要的格式。
生活类比
- 国外电器的插头插不进国内插座,要用一个「转换插头」。
- 朋友讲方言,你听不懂,需要一个“翻译”帮你转成普通话。
解决什么问题
- 老接口、第三方库的返回格式和自己项目想用的不一样。
- 新旧系统切换时,想尽量少改老代码。
什么时候用
- 接了一个旧系统/三方 SDK,返回字段名和你内部统一规范对不上。
- 接口升级,新老字段并存,需要中间层做统一。
JS 示例:用户数据适配器
// 老系统返回的数据
const oldUser = {
name: '张三',
age: 20,
phone: '13800000000'
}
// 新系统期望的数据格式
function adaptUser(old) {
return {
username: old.name,
age: old.age,
mobile: old.phone,
isAdult: old.age >= 18
}
}
const newUser = adaptUser(oldUser)
console.log(newUser)
5. 装饰器模式 (Decorator)
一句话定义
在不改变原对象/函数代码的前提下,给它“套一层壳”增加新的功能。
生活类比
- 一杯美式咖啡:可以加奶、加糖浆、加奶泡,每加一样都包了一层“装饰”,原咖啡还在里面。
- 你给礼物包装:加盒子、加丝带、加贺卡,礼物本身没变,只是外面多了东西。
解决什么问题
- 想给已有函数加日志、异常捕获、耗时统计等功能,但不想/不能改原函数。
- 不同功能可以灵活组合,而不是写很多继承子类。
JS 示例:给函数加日志装饰
function add(a, b) {
return a + b
}
// 装饰器:给任意函数增加日志能力
function withLog(fn) {
return function (...args) {
console.log('[LOG] 调用参数:', args)
const result = fn(...args)
console.log('[LOG] 返回结果:', result)
return result
}
}
const addWithLog = withLog(add)
addWithLog(1, 2)
6. 代理模式 (Proxy)
一句话定义
用一个“代理对象”站在真实对象前面,帮你做权限、缓存、延迟加载等控制。
生活类比
- 明星不会自己接每一个活动电话,都是经纪人先接,再决定要不要转给明星。
- 小区门口保安:不是任何人都能进,要先经过保安这一层判断。
解决什么问题
- 访问某个对象前,需要统一加一层逻辑(鉴权/限流/缓存等)。
- 不想在真实对象里到处写重复的前置检查。
JS 示例:缓存代理
// 原始的“慢函数”,比如从数据库/远程服务器获取数据
async function fetchUserFromServer(id) {
console.log('真正去服务器查:', id)
// 模拟网络延迟
await new Promise(r => setTimeout(r, 500))
return { id, name: '用户' + id }
}
// 代理:加一层缓存
function createUserFetcherWithCache() {
const cache = new Map()
return async function getUser(id) {
if (cache.has(id)) {
console.log('从缓存读取:', id)
return cache.get(id)
}
const user = await fetchUserFromServer(id)
cache.set(id, user)
return user
}
}
const getUser = createUserFetcherWithCache()
7. 外观模式 (Facade)
一句话定义
对外提供一个非常简单的“门面函数”,内部帮你处理一堆复杂的子步骤。
生活类比
- 体检中心“一条龙服务”:你只要在前台说“我要体检套餐 A”,后面排队、抽血、拍片都有人帮你安排。
- 旅行社的“跟团游”:你只管交钱集合,机票、酒店、门票都由旅行社帮你处理。
解决什么问题
- 系统内部流程很复杂,不想每个地方都自己写一遍所有细节。
- 对外暴露太多函数让人头大,希望只有一个入口。
JS 示例:下单外观
function chooseGoods() {
console.log('1. 选择商品')
}
function createOrder() {
console.log('2. 生成订单')
}
function pay() {
console.log('3. 支付')
}
function sendGoods() {
console.log('4. 发货')
}
// 外观函数:对外只暴露 buy
function buy() {
chooseGoods()
createOrder()
pay()
sendGoods()
}
buy()
行为型模式
8. 观察者模式 (Observer)
一句话定义
一个对象(被观察者)维护一组“观察者”,当它自己状态变化时,会主动通知所有观察者。
生活类比
- 你关注了一个博主(被观察者),他一发动态,你就会收到推送。
- 公众号:你点了“关注”,以后每篇文章你都能收到通知。
解决什么问题
- 一个对象变化,需要通知到多个“感兴趣的人”。
- 不想让“被观察者”直接依赖具体的观察者是谁。
什么时候用
- 简单事件机制。
- 某个数据变化,多个 UI 需要联动更新。
JS 示例:自己实现一个最小观察者
class Subject {
constructor() {
this.observers = []
}
addObserver(fn) {
this.observers.push(fn)
}
removeObserver(fn) {
this.observers = this.observers.filter(o => o !== fn)
}
notify(data) {
this.observers.forEach(fn => fn(data))
}
}
const subject = new Subject()
function observerA(msg) {
console.log('观察者A 收到:', msg)
}
subject.addObserver(observerA)
subject.addObserver((msg) => console.log('观察者B 收到:', msg))
subject.notify('有新消息了')
subject.removeObserver(observerA)
subject.notify('第二条消息')
9. 发布-订阅模式 (Pub-Sub)
一句话定义
有一个“中间的事件中心”,发布者向中心发布消息,订阅者向中心订阅消息,发布者和订阅者互相不知道对方是谁。
生活类比
- 微信朋友圈:你发的是给“朋友圈服务器”,不是直接发给每个好友;好友是向微信服务器“订阅”你的动态。
- 电台广播:电台发信号,谁在收听它并不关心。
和观察者的直觉区别
- 观察者:被观察者维护观察者列表,自己一个个通知。
- 发布-订阅:双方都只和“事件中心”打交道,彼此解耦更彻底。
JS 示例:简单事件总线
const EventBus = {
events: {},
on(event, handler) {
(this.events[event] ||= []).push(handler)
},
off(event, handler) {
if (!this.events[event]) return
if (!handler) {
delete this.events[event]
} else {
this.events[event] = this.events[event].filter(h => h !== handler)
}
},
emit(event, data) {
(this.events[event] || []).forEach(h => h(data))
}
}
// 订阅
EventBus.on('order:created', (id) => console.log('客服知道有订单:', id))
EventBus.on('order:created', (id) => console.log('仓库知道有订单:', id))
// 发布
EventBus.emit('order:created', 123)
10. 策略模式 (Strategy)
一句话定义
把一组“可互相替换的算法/规则”封装成独立策略对象(或函数),用时根据条件选择其中一种。
生活类比
- 出门:打车、地铁、骑车、走路,都是“从 A 到 B”的不同“策略”。
- 算工资:按小时、按月、按项目提成,也都是“结算工资”的不同策略。
解决什么问题
- 很多
if...else或switch判断类型,然后执行不同逻辑。 - 想在不改调用代码的前提下,灵活增删算法。
JS 示例:打折策略
const discountStrategies = {
none(price) {
return price
},
vip(price) {
return price * 0.8
},
coupon20(price) {
return price - 20
}
}
function calcPrice(price, type = 'none') {
const strategy = discountStrategies[type] || discountStrategies.none
return strategy(price)
}
console.log(calcPrice(100, 'vip')) // 80
console.log(calcPrice(100, 'coupon20')) // 80
11. 命令模式 (Command)
一句话定义
把一次“操作”封装成一个对象,里面记录“怎么执行”和“怎么撤销”,方便排队、记录历史、撤销重做。
生活类比
- 你桌上的便签本,每张便签写一个待办:可以执行、也可以划掉表示撤销。
- Photoshop 里的“历史记录”:每一步操作都可以撤回、重做。
解决什么问题
- 需要记录一系列操作,支持撤销和重做。
- 需要把“请求的发送者”和“实际执行者”解耦。
JS 示例:可撤销的加减操作
class Command {
constructor(doFn, undoFn) {
this.doFn = doFn
this.undoFn = undoFn
}
execute() { this.doFn() }
undo() { this.undoFn() }
}
class CommandManager {
constructor() {
this.history = []
}
run(command) {
command.execute()
this.history.push(command)
}
undo() {
const cmd = this.history.pop()
cmd && cmd.undo()
}
}
let value = 0
const manager = new CommandManager()
// 加 5 的命令
const add5 = new Command(
() => { value += 5 },
() => { value -= 5 }
)
// 减 3 的命令
const minus3 = new Command(
() => { value -= 3 },
() => { value += 3 }
)
manager.run(add5)
manager.run(minus3)
console.log(value) // 2
manager.undo()
console.log(value) // 5
12. 状态模式 (State)
一句话定义
把一个对象在不同状态下的行为拆成不同的“状态类”,通过切换状态对象来切换行为。
生活类比
- 红绿灯:红灯只能“停”,绿灯只能“走”,黄灯“减速慢行”。行为取决于当前状态。
- 订单:待支付、已支付、已发货、已完成,不同状态能做的操作不同。
解决什么问题
- 充满
if (status === 'xxx') { ... } else if (...) { ... }的庞大分支。 - 状态越来越多时,修改变得非常痛苦。
JS 示例:简化版订单状态
class PendingState {
constructor(order) { this.order = order }
pay() {
console.log('支付成功')
this.order.setState(this.order.paidState)
}
}
class PaidState {
constructor(order) { this.order = order }
ship() {
console.log('已发货')
this.order.setState(this.order.shippedState)
}
}
class ShippedState {
constructor(order) { this.order = order }
ship() {
console.log('已经发过货了,不能重复发货')
}
}
class Order {
constructor() {
this.pendingState = new PendingState(this)
this.paidState = new PaidState(this)
this.shippedState = new ShippedState(this)
this.state = this.pendingState
}
setState(state) {
this.state = state
}
pay() { this.state.pay && this.state.pay() }
ship() { this.state.ship && this.state.ship() }
}
const order = new Order()
order.pay() // 支付成功
order.ship() // 已发货
order.ship() // 已经发过货了,不能重复发货
13. 中介者模式 (Mediator)
一句话定义
用一个“中介对象”来封装多个对象之间的交互逻辑,让这些对象不直接引用彼此。
生活类比
- 房产中介:房东和租客都只跟中介打交道,不直接互相谈。
- 群聊里的“群管理员”:有人申请入群、有人发广告,都由管理员协调处理。
解决什么问题
- 多个对象彼此互相调用,关系变得像“蜘蛛网”一样复杂。
- 需要集中管理一组对象之间的协作,而不是分散在各个对象内部。
JS 示例:聊天室中介者(简化版)
class ChatRoom {
constructor() {
this.users = {}
}
register(user) {
this.users[user.name] = user
user.chatRoom = this
}
send(message, from, toName) {
if (toName) {
const toUser = this.users[toName]
toUser && toUser.receive(message, from)
} else {
// 群发
Object.values(this.users).forEach(user => {
if (user.name !== from.name) {
user.receive(message, from)
}
})
}
}
}
class User {
constructor(name) {
this.name = name
this.chatRoom = null
}
send(message, toName) {
this.chatRoom && this.chatRoom.send(message, this, toName)
}
receive(message, from) {
console.log(`${this.name} 收到来自 ${from.name} 的消息: ${message}`)
}
}
const room = new ChatRoom()
const alice = new User('Alice')
const bob = new User('Bob')
room.register(alice)
room.register(bob)
alice.send('你好,Bob', 'Bob')
bob.send('大家好', null) // 群发
设计模式快速选择指南
| 场景/需求 | 推荐模式 |
|---|---|
| 需要一个全局唯一对象(配置、日志、缓存等) | 单例模式 |
| 需要根据条件创建不同类型对象 | 工厂模式 |
| 需要链式配置构造复杂对象 | 建造者模式 |
| 老数据/老接口格式不统一,需要转换 | 适配器模式 |
| 想给函数或对象“加功能”但不改原代码 | 装饰器模式 |
| 访问对象前统一做权限/缓存/日志等 | 代理模式 |
| 内部流程复杂,对外只想暴露一个简单入口 | 外观模式 |
| 一个对象变化,要通知多个“监听方” | 观察者模式 |
| 需要一个全局事件中心做解耦通信 | 发布-订阅模式 |
| 充满 if-else/switch 的“不同规则” | 策略模式 |
| 需要记录历史、支持撤销/重做 | 命令模式 |
| 各种 status 分支太多,希望按状态拆分逻辑 | 状态模式 |
| 多个对象之间错综复杂的交互 | 中介者模式 |
学习建议
-
先记住生活类比,再看代码
例如:- 单例 = “一个中心”;工厂 = “后厨”;建造者 = “点奶茶”;
- 适配器 = “转换插头”;装饰器 = “给咖啡加料”;代理 = “经纪人”;
- 外观 = “体检套餐”;观察者/发布订阅 = “关注+朋友圈”;
- 策略 = “出行方式”;命令 = “待办便签”;状态 = “红绿灯”;中介者 = “房产中介”。
-
遇到真实场景再反推模式
- 写 if-else 写到崩溃 → 想想是不是可以用“策略模式”。
- 多个地方用到全局配置 → 想想是不是该做成“单例”。
- 一个操作要支持撤销/重做 → 想想是不是用“命令模式”。
-
先会用,再搞懂名字
实战里你可以先按“套路”写出来,之后再对照名字记忆:
“哦,原来我之前写的这个,其实就是 XXX 模式。”