常用设计模式汇总,告诉你如何学习设计模式
Category(分类): JavaScript Status: 已整理
前言:先学会解决问题,再记模式名称
往期文章常把设计模式画成一张很大的思维导图。最开始学习设计模式时,我也读过经典的《设计模式:可复用面向对象软件的基础》:第一遍没看懂,又看一遍,23 个模式前后读了几次,还做了笔记,后来……好像全忘了。
等到面试或者跳槽,再翻出来,只记得工厂和单例,其他的又忘光了。问题不一定在于模式太多,而在于一次看太多、没有把它们放进真实问题里练习。比较稳妥的方式是先掌握少量常见思想,再用它们重构重复流程、可变算法和对象创建代码。剩下的模式等遇到相应问题时再学习。
本文保留企鹅故事,并重点展开模板方法、策略、创建/工厂三条主线。思维导图里的代理、组合、装饰器、Builder 等只是目录,不代表本文已经逐一实现十种模式。

图片来源:原文思维导图,已本地化为
images/98-image-01.webp。原图来自第三方微信图床,转载、图中字体和商标授权未核实;本地化不等于取得授权。
温馨提示:设计模式是可复用的设计语言,不是必须套用的清单。借鉴思想,好用最重要。
模板方法与策略模式
先讲一个企鹅笑话:记者去南极采访企鹅,问第一只企鹅每天做什么,回答是“吃饭,睡觉,打豆豆”。问了 99 只,答案都一样。第 100 只企鹅只说“吃饭,睡觉”。记者问它为什么不打豆豆,企鹅撇嘴说:“我就是豆豆!”
假设三只企鹅都遵循“吃饭、睡觉、打豆豆”,只有最后一步不同。重复复制整段流程当然能工作,但变化点一多,修改公共步骤就要改很多处。
最初的重复代码:Java 对照
下面保留原文的 Java 版本。它是跨语言对照,不是 JavaScript 代码;每个 public class 通常应放在与类名匹配的独立 .java 文件中。
public class LittlePenguin {
public void everyDay() {
System.out.println("吃饭");
System.out.println("睡觉");
System.out.println("用小翅膀打豆豆");
}
}
public class MiddlePenguin {
public void everyDay() {
System.out.println("吃饭");
System.out.println("睡觉");
System.out.println("用圆圆的肚子打豆豆");
}
}
public class BigPenguin {
public void everyDay() {
System.out.println("吃饭");
System.out.println("睡觉");
System.out.println("拿鸡毛掸子打豆豆");
}
}
Java 代码里的 public、System.out.println 和类继承都属于 Java 语法。把它复制到 .js 文件中不会运行。
先抽取独立步骤
也可以在 Java 中把吃饭、睡觉和打豆豆分别抽成方法;这能让步骤更清晰,但调用方仍然要自己保证执行顺序:
public class LittlePenguin {
public void eating() {
System.out.println("吃饭");
}
public void sleeping() {
System.out.println("睡觉");
}
public void beating() {
System.out.println("用小翅膀打豆豆");
}
}
Java 的示例省略其他企鹅类。下面改用 JavaScript 主线,因为 JavaScript 的函数是一等值,固定流程和变化步骤可以直接用函数表达,不必先建立一组类。
模板方法:固定骨架,开放少量步骤
经典 GoF 模板方法的意图是:在一个方法中定义算法骨架,把某些步骤延迟给子类或钩子实现。经典的继承实现通常由基类安排步骤顺序,子类只改变允许变化的步骤。
Java 版:跨语言对照
原文的 Java 版本还可以稍微修正:如果希望 everyDay() 真正不能被子类覆盖,应在 Java 中使用 final;普通的 public 方法并不能保证固定流程。
public abstract class Penguin {
public final void everyDay() {
eating();
sleeping();
beating();
}
protected final void eating() {
System.out.println("吃饭");
}
protected final void sleeping() {
System.out.println("睡觉");
}
protected abstract void beating();
}
public final class LittlePenguin extends Penguin {
@Override
protected void beating() {
System.out.println("用小翅膀打豆豆");
}
}
这是 Java 语境下的模板方法示意。JavaScript 没有普通运行时意义上的 abstract 和 final 关键字;TypeScript 的 abstract 主要是类型检查,也不能把编译后的 JavaScript 公共方法变成不可覆盖的方法。
JavaScript 函数式模板
在 JavaScript 中,可以把固定顺序写成一个函数,把变化步骤作为参数。这个实现保留了模板方法的意图,但不是必须使用继承:
function assertFunction(value, name) {
if (typeof value !== 'function') {
throw new TypeError(`${name} 必须是函数`)
}
return value
}
// 骨架由这里固定,beat 是可替换的钩子。
function dailyRoutine(beat) {
assertFunction(beat, 'beat')
return () => [
'吃饭',
'睡觉',
beat(),
]
}
const littleRoutine = dailyRoutine(() => '用小翅膀打豆豆')
console.log(littleRoutine().join(','))
// 吃饭,睡觉,用小翅膀打豆豆
这个版本的好处是流程函数容易测试,返回数组也比直接打印更容易组合。如果项目确实需要多态对象,也可以使用 class;但不要把“使用继承”说成 JavaScript 实现模板方法的唯一方式。

图片来源:原文模板方法配图,已本地化。原图来自第三方微信图床,转载授权未核实;图片仅作概念对照,正文以代码语义为准。
策略模式:把可替换算法交给上下文
策略模式的核心是:把同一职责的多个算法分开,让上下文依赖一个稳定的策略契约并委托执行。策略可以是对象,也可以是 JavaScript 函数;是否在运行期间切换是可选能力,不是模式的必要条件。
原文用企鹅对象同时表示吃饭、睡觉和打豆豆,这其实把模板流程和策略混在了一起。为了突出策略本身,先只替换“打豆豆”的算法:
function createBehaviorContext(initialStrategy) {
let strategy = assertFunction(initialStrategy, 'strategy')
return {
setStrategy(nextStrategy) {
strategy = assertFunction(nextStrategy, 'strategy')
},
run(...args) {
return strategy(...args)
},
}
}
const littleBeat = () => '用小翅膀打豆豆'
const middleBeat = () => '用圆圆的肚子打豆豆'
const bigBeat = () => '拿鸡毛掸子打豆豆'
const behavior = createBehaviorContext(littleBeat)
console.log(behavior.run())
behavior.setStrategy(middleBeat)
console.log(behavior.run())
behavior.setStrategy(bigBeat)
console.log(behavior.run())
策略也可以携带配置和私有状态:
function createPrefixStrategy(prefix) {
return (name) => `${prefix}${name}`
}
const polite = createPrefixStrategy('请 ')
console.log(polite('打豆豆'))
这里闭包只是实现策略的一种手段。若策略在创建上下文时就确定并且不会改变,可以去掉 setStrategy;若每个请求都选择不同算法,也可以把函数作为请求参数传入。选择时机取决于生命周期和业务需求。

图片来源:原文策略模式配图,已本地化。原图来自第三方微信图床,转载授权未核实;请在获得许可后再用于公开发布。
模板方法和策略怎么选
它们的意图和实现关系并不完全相同,也不必只能二选一。下面的区别是常见启发式,不是语言规范:
| 关注点 | 模板方法 | 策略 |
|---|---|---|
| 主要意图 | 固定一套流程,开放少数钩子 | 替换某个完整职责或算法族 |
| 经典 GoF 关系 | 常用继承表达骨架和步骤 | 常用组合/委托表达算法替换 |
| JavaScript 写法 | 函数式模板、闭包、类都可以 | 一个函数、闭包或带 execute 的对象都可以 |
| 选择时机 | 变体常由具体实现决定 | 可以在构造时、请求时或运行期间选择 |
| 耦合 | 继承版通常耦合较紧 | 委托版通常更松,但仍取决于契约 |
如果只有一个流程,且顺序不变、变化点很少,函数式模板通常足够;如果同一职责有多个算法并且调用方需要替换,策略更直观;两者也可以组合。不要因为“模式名”而增加不必要的类。
实际场景
框架代码经常有固定的 process() 流程,其中 preProcess() 可能是可选钩子;这可以表达模板方法的思想。支付、折扣、排序、校验等场景有多个互换算法,则可以使用策略。最终要看重复的流程、变化的边界、测试成本和对象生命周期,而不是把模式硬套成固定的选择时机。
工厂、创建函数和注册表
先看一个跨语言的分支例子
原文这里的 switch 使用的是 PHP 风格语法,保留它是为了说明历史业务场景;它不是 JavaScript,也不是 Java 完整程序:
switch ($taskInfo['type_id']) {
case 1:
$result = batchFrozen($rowKey, 1);
break;
case 2:
$result = batchFrozen($rowKey, 0);
break;
case 3:
$result = batchReshipment($rowKey);
break;
case 4:
$result = batchCancel($rowKey);
break;
default:
throw new InvalidArgumentException('未知任务类型');
}
注册表可以把变体和创建逻辑集中起来,减少业务调用方中的长分支;但选择逻辑并没有消失,未知类型仍然需要明确的错误策略。只有两个稳定分支时,清楚的 if 或 switch 可能更易读,不必为了“去掉 if/else”而引入工厂。
名称要分清:简单工厂、注册表和 Factory Method
- 工厂函数/简单工厂:一个函数根据输入创建或返回产品。
- 注册表式工厂:用
Map、对象或容器登记类型到创建器的映射。 - GoF Factory Method:经典结构中,Creator 提供创建方法,具体 Creator 子类通过覆写该方法改变所创建的 Product 类型。
- Abstract Factory:创建一组相互匹配的产品,不等同于任意一个
createX()函数。
原文的 Java Map<String, Penguin> 保存的是已经创建好的实例,因此更准确的名字是“实例注册表/缓存式简单工厂”,不是严格意义上的 GoF Factory Method;每次取出的是同一个对象,可能共享可变状态。
JavaScript 的 Map 创建器注册表
下面的示例把 Map 中的值设为创建函数,因此每次调用都会得到新对象。它可以在 Node.js 或浏览器控制台运行:
function routineFor(beat) {
return () => ['吃饭', '睡觉', beat()]
}
const penguinCreators = new Map([
['little', () => ({
kind: 'little',
everyDay: routineFor(() => '用小翅膀打豆豆'),
})],
['middle', () => ({
kind: 'middle',
everyDay: routineFor(() => '用圆圆的肚子打豆豆'),
})],
['big', () => ({
kind: 'big',
everyDay: routineFor(() => '拿鸡毛掸子打豆豆'),
})],
])
function createPenguin(kind) {
const creator = penguinCreators.get(kind)
if (!creator) {
throw new RangeError(`未知企鹅类型:${kind}`)
}
return creator()
}
const little = createPenguin('little')
console.log(little.everyDay().join(','))
console.log(createPenguin('little') !== createPenguin('little'))
// 吃饭,睡觉,用小翅膀打豆豆
// true
如果产品本来就无状态,并且明确希望复用同一个实例,也可以登记实例;但应在文档中写清缓存/单例语义。Map.prototype.get() 未命中返回 undefined,应在工厂边界尽早抛出可读错误,而不是让调用方稍后遇到难定位的异常。

图片来源:原文工厂模式配图,已本地化。原图来自第三方微信图床,转载授权未核实;不得把本地镜像视为已获得再发布许可。
Java 工厂示例:跨语言历史对照
原文的 Java 示例大意如下。它用抽象类复用日常流程,再通过静态 Map 取企鹅实例;这里加上了语言和生命周期说明:
import java.util.Map;
import java.util.function.Supplier;
public final class PenguinFactory {
private static final Map<String, Supplier<Penguin>> CREATORS = Map.of(
"little", LittlePenguin::new,
"middle", MiddlePenguin::new,
"big", BigPenguin::new
);
public static Penguin create(String name) {
Supplier<Penguin> creator = CREATORS.get(name);
if (creator == null) {
throw new IllegalArgumentException("未知企鹅类型:" + name);
}
return creator.get();
}
}
这段 Java 代码使用 Map 和 Supplier 返回新实例,仍然是注册表式简单工厂;它没有 Creator 子类覆写 Factory Method。@Autowired 静态字段和静态代码块也不能当作 Spring 实例注入的可靠写法。真实 Spring 应使用构造器注入,并明确 Bean 是否有状态。
文章开头任务场景的改写思路
原文还展示了把几十个任务 case 放到 TaskFactory 中的做法。那段代码依赖 AbstractTask、ParamWrapper、Spring 注解等未定义类型,只能当作 Java/Spring 伪代码,不能当作独立 JavaScript 示例。用 JavaScript 表达同一思想时,可以登记函数:
const taskHandlers = new Map([
['frozen', (rowKey) => batchFrozen(rowKey, 1)],
['unfrozen', (rowKey) => batchFrozen(rowKey, 0)],
['reshipment', (rowKey) => batchReshipment(rowKey)],
['cancel', (rowKey) => batchCancel(rowKey)],
])
function runTask(type, rowKey) {
const handler = taskHandlers.get(type)
if (!handler) {
throw new RangeError(`未知任务类型:${type}`)
}
return handler(rowKey)
}
这里的 batchFrozen 等函数代表业务依赖,示例重点是注册和错误边界。注册表减少了调用方的分支,但新增任务仍要注册映射;它不是“消灭所有条件判断”的魔法。
选择检查表
| 遇到的问题 | 可以先考虑 |
|---|---|
| 步骤顺序固定,只有少量受控钩子变化 | 模板方法或函数式模板 |
| 同一职责有多个算法,调用方可能按请求替换 | 策略函数、策略对象或组合 |
| 构造参数、依赖、产品类型或生命周期需要集中管理 | 工厂函数、创建方法或注册表 |
| 只有一两个稳定分支且没有复用需求 | 清楚的 if/switch |
设计模式的价值是帮助你命名和隔离变化,不是证明某种写法“唯一正确”。先写出可读的代码,再根据重复、变化、测试和生命周期决定是否抽象。