Appearance
设计模式
导航目录
一、设计原则篇
二、创建型模式
三、结构型模式
四、行为型模式
五、总结
SOLID 五大设计原则
核心概念
SOLID 是面向对象设计中的五个核心原则,遵循它们能编写出高内聚、低耦合、易扩展、易维护的代码。前端在弱类型的 JS 中虽不必严格套用,但其思想对组件设计、状态管理、模块拆分都极具指导意义。
| 原则 | 全称 | 核心含义 |
|---|---|---|
| S | 单一职责原则(Single Responsibility) | 一个模块/类只负责一件事 |
| O | 开放封闭原则(Open/Closed) | 对扩展开放,对修改封闭 |
| L | 里氏替换原则(Liskov Substitution) | 子类可以替换父类且行为一致 |
| I | 接口隔离原则(Interface Segregation) | 接口应精简,避免"胖接口" |
| D | 依赖倒置原则(Dependency Inversion) | 面向抽象编程,不依赖具体实现 |
单一职责原则 S
一个程序只做好一件事。如果功能过于复杂就拆分,每个部分保持独立。例如一个 React 组件只负责渲染,数据获取抽离到 Hook 或 Service。
开放封闭原则 O
对扩展开放,对修改封闭。增加需求时,应通过扩展新代码实现,而非修改已有代码。这是软件设计的终极目标,也是最重要的原则。
里氏替换原则 L
子类能覆盖父类,父类出现的地方子类都能替换且不破坏程序。JS 中因弱类型、继承使用较少,应用不多,但理解其"子类不应削弱父类契约"的思想很有价值。
接口隔离原则 I
保持接口的单一独立,避免"胖接口"。JS 没有接口概念(TypeScript 例外),使用较少。它类似单一职责原则,但更关注接口层面的粒度。
依赖倒置原则 D
面向抽象(接口)编程,依赖于抽象而不依赖于具体实现。使用方只关注接口约定,不关注具体类的实现,从而实现解耦(如依赖注入 DI)。
掌握重点
S 和 O 在前端体现最多,应重点理解;L、I、D 了解其设计用意即可。
用 Promise 说明单一职责与开放封闭
核心概念
Promise 的链式调用是 SOLID 原则的绝佳示例:每个 then 只做一件事(单一职责),新增需求时扩展 then 链而非修改已有逻辑(开放封闭)。
js
function loadImg(src) {
return new Promise(function (resolve, reject) {
const img = document.createElement("img");
img.onload = function () {
resolve(img);
};
img.onerror = function () {
reject(new Error("图片加载失败"));
};
img.src = src;
});
}
const src = "https://www.imooc.com/static/img/index/logo.png";
loadImg(src)
.then(function (img) {
console.log("img.width", img.width); // 单一职责:只处理宽度
return img;
})
.then(function (img) {
console.log("img.height", img.height); // 扩展新的 then,不修改上面的逻辑
})
.catch(function (err) {
console.error(err); // 统一错误处理
});23 种设计模式总览
核心概念
设计模式是软件设计中反复出现问题的通用解决方案,由 GoF(四人组)总结为 23 种,按目的分为创建型、结构型、行为型三大类。
三大分类
| 分类 | 关注点 | 包含模式 |
|---|---|---|
| 创建型 | 对象的创建过程 | 工厂、单例、原型、建造者、抽象工厂 |
| 结构型 | 类或对象的组合关系 | 适配器、装饰器、代理、外观、桥接、组合、享元 |
| 行为型 | 对象间的通信与职责分配 | 策略、模板方法、观察者、迭代器、责任链、命令、备忘录、状态、访问者、中介者、解释器 |
前端高频模式
带 ★ 的是前端开发中出现频率最高、最值得深入掌握的模式: 工厂 ★、单例 ★、代理 ★、观察者/发布订阅 ★、装饰器 ★、策略 ★、状态 ★
工厂模式
核心概念
工厂模式将对象的创建过程封装起来,客户端无需直接使用 new,而是通过工厂方法获取对象,实现"创建"与"使用"的解耦。
核心思想
- 将
new操作单独封装 - 当代码中出现大量
new且创建逻辑复杂时,就应考虑工厂模式
生活类比
你去快餐店买汉堡,直接点餐、取餐,不会自己动手做。商店"封装"了做汉堡的过程,做好后直接交给顾客。
前端应用场景
- jQuery ——
$('div')内部返回 jQuery 对象 - React ——
React.createElement创建虚拟 DOM - Vue —— 异步组件工厂函数
代码示例
js
class Product {
constructor(name) {
this.name = name;
}
init() {
console.log("init");
}
fn() {
console.log("fn");
}
}
class Factory {
create(name) {
return new Product(name);
}
}
const factory = new Factory();
const p = factory.create("product");
p.init();
p.fn();优缺点
优点:封装创建逻辑,降低耦合,便于统一管理与扩展。 缺点:增加类的数量,简单场景下可能过度设计。
单例模式
核心概念
单例模式确保一个类只有一个实例,并提供一个全局访问点。适用于全局唯一、状态共享的场景。
前端应用场景
- 登录框 / 弹窗(全局只需一个)
- 购物车
- Vuex / Redux 中的 store
- 全局缓存、全局配置
基础实现
js
class Singleton {
constructor(name) {
this.name = name;
}
init() {
console.log(`${this.name}-init`);
}
}
Singleton.getInstance = (function () {
let instance;
return function (name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
};
})();
const a = Singleton.getInstance("单例");
const b = Singleton.getInstance("单例");
console.log(a === b); // true,始终是同一个实例实战:登录框单例
js
class LoginForm {
constructor() {
this.state = "hidden";
}
show() {
if (this.state === "show") {
console.log("已经显示了");
return;
}
this.state = "show";
console.log("显示登录框");
}
hidden() {
if (this.state === "hidden") {
console.log("已经隐藏了");
return;
}
this.state = "hidden";
console.log("隐藏登录框");
}
}
LoginForm.getInstance = (function () {
let instance;
return function () {
if (!instance) {
instance = new LoginForm();
}
return instance;
};
})();
const login1 = LoginForm.getInstance();
const login2 = LoginForm.getInstance();
console.log(login1 === login2); // true优缺点
优点:全局唯一入口,避免重复创建,节省资源。 缺点:属于全局状态,过度使用会增加模块间隐式耦合,不利于测试。
原型模式
核心概念
原型模式通过克隆现有对象来创建新对象,而非通过实例化类。JS 本身就是基于原型的语言,Object.create 是其最直接的体现。
代码示例
js
const prototype = {
getName: function () {
console.log(this.name);
},
};
// 以 prototype 为原型创建新对象
const objA = Object.create(prototype);
objA.name = "tom";
objA.getName(); // tom
const objB = Object.create(prototype);
objB.name = "kiss";
objB.getName(); // kiss前端体现
JS 的原型链(__proto__ / prototype)本身就是原型模式的应用。Object.assign、浅/深拷贝也常用于对象克隆场景。
适配器模式
核心概念
适配器模式用于解决接口不兼容的问题,通过中间层(适配器)将一个接口转换为使用者期望的另一个接口,使原本不兼容的类能协同工作。
生活类比
各国插座标准不同,通过一个"电源转换插头"(适配器),就能让中国插头在德国插座上使用。
前端应用场景
- 封装/兼容旧接口,让老代码适配新系统
- Vue 的
computed(把数据源适配成视图需要的形态) - 第三方库返回数据格式与业务不符时的转换层
代码示例
js
// 旧接口(不兼容)
class Adaptee {
oldRequest() {
return "德国标准插头";
}
}
// 适配器:把旧接口转换成期望的新接口
class Adapter {
constructor() {
this.adaptee = new Adaptee();
}
request() {
const info = this.adaptee.oldRequest();
console.log(`${info} >>> 转换为中国插头`);
}
}
const adapter = new Adapter();
adapter.request();装饰器模式
核心概念
装饰器模式允许在不改变现有对象结构的前提下,动态地为对象添加新功能,是"对扩展开放、对修改封闭"的典型实践。
代码示例
js
class Circle {
draw() {
console.log("画一个圆");
}
}
class Decorator {
constructor(circle) {
this.circle = circle;
}
draw() {
this.circle.draw(); // 保留原功能
this.setColor(); // 附加新功能
}
setColor() {
console.log("给圆设置颜色");
}
}
const circle = new Circle();
circle.draw(); // 原始功能
const decorated = new Decorator(circle);
decorated.draw(); // 增强后的功能前端体现
React 的高阶组件(HOC)、ES7 装饰器语法都是装饰器模式的应用。
ES7 装饰器
核心概念
ES7 装饰器(@decorator)是一种语法糖,本质是一个函数,用于修改类、方法或属性的行为。TypeScript 与 NestJS、Angular 大量使用。
装饰器函数的三个参数
- target:被装饰的目标对象
- key:被装饰的属性或方法名
- descriptor:属性描述符(对属性/方法的约束)
装饰器的类型
| 类型 | target 是什么 | 说明 |
|---|---|---|
| 类装饰器 | 类本身 | 新增的属性/方法挂到类的静态属性上 |
| 方法装饰器 | 类的原型(或构造函数) | key 为方法名,可改写方法逻辑 |
| 属性装饰器 | 类的原型(或构造函数) | key 为属性名,约束属性 |
执行顺序
- 靠近类/方法的装饰器先执行(由内向外)
- 若为工厂函数返回的装饰器,工厂函数从上到下执行,装饰逻辑仍由内向外
类装饰器
js
// target 就是被装饰的类本身
function isAnimal(target) {
target.isAnimal = true;
}
@isAnimal
class Monkey {}
console.log(Monkey.isAnimal); // true
// 工厂模式的装饰器(可传参)
function isAnimalFactory(flag) {
return function (target) {
target.isAnimal = flag;
};
}
@isAnimalFactory(true)
class Cat {}
console.log(Cat.isAnimal); // true混入新功能(mixins)
js
function mixins(list) {
return function (target) {
Object.assign(target.prototype, list);
};
}
const foo = {
add() {
console.log("add");
},
};
@mixins(foo)
class Utils {}
new Utils().add(); // add方法装饰器(日志增强)
js
function addLog(target, key, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.log(`${key} 被调用,参数:`, args);
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@addLog
add(a, b) {
return a + b;
}
}
new Calculator().add(10, 10); // add 被调用,参数: [10, 10] → 返回 20代理模式
核心概念
代理模式为一个对象提供代理对象,由代理控制对原对象的访问。类似生活中的"中介"——你不直接接触卖家,而是通过中介完成交易。
生活类比
买二手车时,你不必亲自找车源、验车、办过户,而是委托中介公司代办,你只负责选车和付钱。
前端应用场景
- 图片懒加载(先用占位图代理,真图加载完再替换)
- 事件代理 / 事件委托
- ES6 Proxy 实现数据劫持(Vue3 响应式)
- 缓存代理(缓存计算结果)
代码示例
js
// 真实对象
class RealImg {
constructor(fileName) {
this.fileName = fileName;
this.loadFromDisk();
}
display() {
console.log(`显示图片:${this.fileName}`);
}
loadFromDisk() {
console.log(`从磁盘加载:${this.fileName}`);
}
}
// 代理对象:控制真实对象的创建与访问
class ProxyImg {
constructor(fileName) {
this.fileName = fileName;
}
display() {
if (!this.realImg) {
this.realImg = new RealImg(this.fileName); // 延迟加载
}
this.realImg.display();
}
}
const img = new ProxyImg("logo.png");
img.display(); // 首次访问才真正加载
img.display(); // 复用已加载的真实对象ES6 Proxy 代理
核心概念
ES6 提供原生 Proxy 构造函数,可拦截对象的读取、赋值等操作,是 Vue3 响应式系统的底层基础。
代码示例
js
const star = {
name: "刘德华",
age: 55,
phone: "13700000000",
};
const agent = new Proxy(star, {
get(target, key) {
if (key === "phone") return "13788888888"; // 返回经纪人电话,保护隐私
if (key === "price") return 2000000; // 出场价
return target[key];
},
set(target, key, value) {
if (key === "price") {
if (value < 2000000) {
throw new Error("出场价不能低于 2000000");
}
console.log("成交");
target[key] = value;
return true;
}
return true;
},
});
console.log(agent.phone); // 13788888888
console.log(agent.price); // 2000000
agent.price = 3000000; // 成交与 defineProperty 的区别
Vue2 使用 Object.defineProperty 逐个劫持属性(无法监听新增属性、数组索引);Vue3 改用 Proxy 代理整个对象,能拦截更多操作,性能与能力都更强。
代理 / 适配器 / 装饰器对比
核心概念
这三者都是结构型模式,都在原对象外"包一层",但目的与接口形态截然不同。
| 模式 | 接口是否改变 | 目的 |
|---|---|---|
| 适配器模式 | 改变(提供不同接口) | 兼容不匹配的接口 |
| 代理模式 | 不变(提供相同接口) | 控制访问(如延迟、缓存、权限) |
| 装饰器模式 | 不变(提供相同接口) | 扩展新功能,原功能不变 |
一句话记忆
- 适配器:接口不兼容 → 转换成能用的
- 代理:接口一样 → 但访问被"经过处理"(如懒加载、鉴权)
- 装饰器:接口一样 → 但功能被"叠加增强"
外观模式
核心概念
外观模式(Facade)为子系统中的一组接口提供一个统一的高层接口,隐藏内部复杂性,让子系统更易使用。
核心思想
- 为复杂子系统提供一个简单的上层入口
- 客户端只与外观交互,无需了解内部细节
代码示例
经典的兼容性事件绑定就是外观模式——屏蔽浏览器差异,对外只暴露一个 addEvent:
js
function addEvent(el, type, fn) {
if (el.addEventListener) {
el.addEventListener(type, fn, false); // 标准浏览器
} else if (el.attachEvent) {
el.attachEvent("on" + type, fn); // 旧版 IE
} else {
el["on" + type] = fn; // 兜底
}
}前端体现
jQuery 的 $.ajax、axios 的统一请求封装、各种 SDK 的初始化入口,都是外观模式的应用。
桥接模式
核心概念
桥接模式将抽象部分与实现部分分离,使它们可以独立变化,避免多维度变化导致的类爆炸。
核心思想
当一个类存在两个(或多个)独立变化的维度时(如"形状"与"颜色"),用组合代替继承,用一座"桥"连接两个维度。
代码示例
js
// 维度一:颜色
class Color {
constructor(name) {
this.name = name;
}
}
// 维度二:形状,通过组合持有颜色(桥接)
class Shape {
constructor(name, color) {
this.name = name;
this.color = color;
}
draw() {
console.log(`${this.color.name}的${this.name}`);
}
}
const red = new Color("红色");
const circle = new Shape("圆形", red);
circle.draw(); // 红色的圆形优势
形状与颜色可以各自独立扩展。新增"蓝色"或"方形"时,无需修改另一维度,避免了 红色圆形、蓝色方形 这样的组合类爆炸。
组合模式
核心概念
组合模式将对象组合成树形结构表示"整体-部分"层次,使客户端对单个对象和组合对象的使用具有一致性。
前端应用场景
- Vue / React 的虚拟 DOM(VNode) —— 节点可包含子节点,递归渲染
- 多级菜单 / 树形控件
- 文件夹与文件结构
代码示例
js
// 统一接口:无论文件还是文件夹都实现 display
class File {
constructor(name) {
this.name = name;
}
display(prefix = "") {
console.log(prefix + this.name);
}
}
class Folder {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
return this;
}
display(prefix = "") {
console.log(prefix + this.name);
this.children.forEach((child) => child.display(prefix + " "));
}
}
const root = new Folder("src");
root
.add(new File("index.js"))
.add(new Folder("utils").add(new File("http.js")));
root.display();
// src
// index.js
// utils
// http.js享元模式
核心概念
享元模式通过共享大量细粒度对象来减少内存占用,核心是分离对象的内部状态(可共享)与外部状态(不可共享)。
核心思想
- 相同的数据只保存一份,共享使用
- 主要目的是节省内存,而非提升执行效率
代码示例
js
// 内部状态可共享:用工厂缓存已创建的对象
class Book {
constructor(title) {
this.title = title; // 内部状态
}
}
class BookFactory {
constructor() {
this.books = {};
}
get(title) {
if (!this.books[title]) {
this.books[title] = new Book(title); // 同名书只创建一次
}
return this.books[title];
}
}
const factory = new BookFactory();
const b1 = factory.get("JS 高级程序设计");
const b2 = factory.get("JS 高级程序设计");
console.log(b1 === b2); // true,共享同一实例前端体现
事件委托(一个父节点代理大量子节点事件)、大列表渲染中的对象池复用,都是享元思想的应用。
观察者模式
核心概念
定义对象间一对多的依赖关系,当一个对象(目标 Subject)状态发生改变时,所有依赖它的对象(观察者 Observer)都会自动收到通知并更新。
生活类比:你订阅了某个公众号,公众号(目标)一旦发布新文章,所有订阅者(观察者)都会收到推送。
js
// 目标对象:维护观察者列表
class Subject {
constructor() {
this.observers = [];
}
attach(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) this.observers.splice(index, 1);
}
// 状态变化时通知所有观察者
notifyAllObservers(data) {
this.observers.forEach((observer) => observer.update(data));
}
}
// 观察者:实现统一的 update 接口
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} 收到通知:${data}`);
}
}
const subject = new Subject();
const ob1 = new Observer("张三");
const ob2 = new Observer("李四");
subject.attach(ob1);
subject.attach(ob2);
subject.notifyAllObservers("新文章发布了"); // 张三、李四 均收到优缺点
- 优点:目标与观察者松耦合,支持广播通信,符合开闭原则。
- 缺点:观察者过多时通知开销大;若观察者间有依赖,通知顺序难以保证;可能引起循环调用。
前端体现
Vue2 响应式原理(Dep 收集依赖、Watcher 作为观察者,数据变化时 dep.notify() 通知更新)、DOM 事件监听本质都是观察者模式。
发布订阅模式
核心概念
发布者(Publisher)与订阅者(Subscriber)不直接通信,而是通过一个**事件中心(Event Bus / 调度中心)**进行解耦。发布者只管往事件中心发消息,订阅者只管向事件中心注册监听。
js
// 事件中心
class EventEmitter {
constructor() {
this.events = {};
}
on(type, handler) {
(this.events[type] || (this.events[type] = [])).push(handler);
}
off(type, handler) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter((h) => h !== handler);
}
emit(type, ...args) {
(this.events[type] || []).forEach((handler) => handler(...args));
}
once(type, handler) {
const wrap = (...args) => {
handler(...args);
this.off(type, wrap);
};
this.on(type, wrap);
}
}
const bus = new EventEmitter();
bus.on("login", (user) => console.log(`欢迎 ${user}`));
bus.emit("login", "张三"); // 欢迎 张三观察者模式 vs 发布订阅模式
| 维度 | 观察者模式 | 发布订阅模式 |
|---|---|---|
| 通信方式 | 目标直接通知观察者 | 通过事件中心中转 |
| 耦合度 | 目标持有观察者引用,松耦合 | 发布者/订阅者完全解耦,互不感知 |
| 角色 | Subject、Observer | Publisher、EventBus、Subscriber |
| 典型场景 | Vue 响应式、DOM 事件 | Node EventEmitter、全局事件总线 |
前端体现
Node.js 的 EventEmitter、Vue2 的 $emit/$on 全局事件总线、各类状态管理库的事件机制都属于发布订阅。
迭代器模式
核心概念
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。使用者无需关心数据结构,只需通过统一接口(如 next())遍历。
js
// 迭代器
class Iterator {
constructor(container) {
this.list = container.list;
this.index = 0;
}
hasNext() {
return this.index < this.list.length;
}
next() {
return this.list[this.index++];
}
}
// 容器
class Container {
constructor(list) {
this.list = list;
}
getIterator() {
return new Iterator(this);
}
}
const iterator = new Container([1, 2, 3]).getIterator();
while (iterator.hasNext()) {
console.log(iterator.next()); // 1 2 3
}ES6 迭代器:实现 [Symbol.iterator] 即可被 for...of、扩展运算符消费,返回值需满足 { value, done } 结构。
js
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
console.log([...range]); // [1, 2, 3]前端体现
Array、Map、Set、NodeList 均内置迭代器;for...of、解构、Array.from 都依赖迭代器协议。
状态模式
核心概念
允许一个对象在其内部状态改变时改变它的行为,看起来像是修改了它的类。将各状态的行为封装到独立的状态类中,消除大量的 if/else 或 switch 分支。
生活类比:红绿灯,不同状态(红/绿/黄)下"行为"不同,且状态之间按规则流转。
js
// 状态类
class GreenLight {
handle(context) {
console.log("绿灯:通行");
context.setState(new YellowLight());
}
}
class YellowLight {
handle(context) {
console.log("黄灯:请注意");
context.setState(new RedLight());
}
}
class RedLight {
handle(context) {
console.log("红灯:停止");
context.setState(new GreenLight());
}
}
// 上下文:持有当前状态
class Context {
constructor() {
this.state = new GreenLight();
}
setState(state) {
this.state = state;
}
request() {
this.state.handle(this);
}
}
const light = new Context();
light.request(); // 绿灯:通行
light.request(); // 黄灯:请注意
light.request(); // 红灯:停止优缺点
- 优点:消除臃肿的条件分支,状态切换逻辑清晰,符合开闭原则(新增状态只加类)。
- 缺点:状态较多时类数量膨胀;状态间流转关系分散在各状态类中。
前端体现
按钮的多状态(正常/loading/禁用/成功)、播放器(播放/暂停/停止)、审批流状态机都适合状态模式。
策略模式
核心概念
定义一系列算法,把它们一个个封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化,同样用于消除大量条件分支。
生活类比:超市会员打折,不同会员等级(普通/白银/黄金)对应不同的折扣算法。
js
// 策略集合:每种会员对应一个计算函数
const strategies = {
normal: (price) => price,
silver: (price) => price * 0.9,
gold: (price) => price * 0.8,
};
// 使用:根据类型选择策略
function getPrice(level, price) {
return strategies[level](price);
}
console.log(getPrice("normal", 100)); // 100
console.log(getPrice("silver", 100)); // 90
console.log(getPrice("gold", 100)); // 80状态模式 vs 策略模式:两者结构相似(都封装行为消除分支),区别在于——策略之间平行独立、由客户端主动选择;状态之间存在流转关系、由状态内部自动切换。
优缺点
- 优点:算法可自由切换、复用,避免多重条件判断,扩展性好。
- 缺点:客户端需了解所有策略;策略数量多时会产生较多策略对象。
前端体现
表单校验规则、不同支付方式的价格计算、排序算法切换都是策略模式的典型应用。
模板方法模式
核心概念
定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
生活类比:泡茶和泡咖啡流程一致(烧水 → 冲泡 → 倒入杯中 → 加辅料),只有"冲泡"和"加辅料"两步不同。
js
// 抽象基类:定义骨架
class Beverage {
// 模板方法:固定流程
init() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
boilWater() {
console.log("把水煮沸");
}
pourInCup() {
console.log("倒进杯子");
}
// 交给子类实现的钩子
brew() {
throw new Error("子类必须实现 brew");
}
addCondiments() {
throw new Error("子类必须实现 addCondiments");
}
}
class Tea extends Beverage {
brew() {
console.log("用沸水浸泡茶叶");
}
addCondiments() {
console.log("加柠檬");
}
}
new Tea().init(); // 煮水→泡茶叶→倒杯→加柠檬优缺点
- 优点:复用公共流程,符合开闭原则,把不变与可变分离。
- 缺点:每个不同实现都要一个子类,类数量增加;受抽象类约束。
前端体现
组件生命周期(框架定义调用顺序,业务实现具体钩子)、请求封装的统一流程(前置处理 → 请求 → 后置处理)都是模板方法。
责任链模式
核心概念
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合。将这些对象连成一条链,并沿着这条链传递请求,直到有对象处理它为止。
生活类比:请假审批,组长处理不了转主管,主管处理不了转总监,层层上报。
js
class Handler {
setNext(handler) {
this.next = handler;
return handler; // 支持链式设置
}
handle(days) {
if (this.next) return this.next.handle(days);
console.log("无人可审批");
}
}
class Leader extends Handler {
handle(days) {
if (days <= 1) return console.log("组长批准");
return super.handle(days);
}
}
class Manager extends Handler {
handle(days) {
if (days <= 3) return console.log("主管批准");
return super.handle(days);
}
}
class Boss extends Handler {
handle(days) {
console.log("老板批准");
}
}
const leader = new Leader();
leader.setNext(new Manager()).setNext(new Boss());
leader.handle(1); // 组长批准
leader.handle(3); // 主管批准
leader.handle(7); // 老板批准优缺点
- 优点:解耦发送者与接收者,动态增减处理节点,符合单一职责。
- 缺点:请求可能到链尾仍未被处理;链过长时影响性能与调试。
前端体现
Express/Koa 中间件洋葱模型、事件冒泡、多级表单校验、拦截器链都是责任链的体现。
命令模式
核心概念
将一个请求封装成一个对象,从而使你可用不同的请求对客户端进行参数化。请求的发起者与执行者解耦,并支持撤销、排队、日志等操作。
js
// 接收者:真正执行操作
class Receiver {
execute() {
console.log("执行操作");
}
undo() {
console.log("撤销操作");
}
}
// 命令:封装请求
class Command {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.execute();
}
undo() {
this.receiver.undo();
}
}
// 调用者:只管触发,不关心细节
class Invoker {
constructor(command) {
this.command = command;
}
invoke() {
this.command.execute();
}
cancel() {
this.command.undo();
}
}
const invoker = new Invoker(new Command(new Receiver()));
invoker.invoke(); // 执行操作
invoker.cancel(); // 撤销操作优缺点
- 优点:发起者与执行者解耦,易于实现撤销/重做、宏命令、命令队列。
- 缺点:每个具体命令都需一个类,可能导致类膨胀。
前端体现
编辑器的撤销/重做栈、按钮点击与业务解耦、批量操作命令队列都是命令模式。
中介者模式
核心概念
用一个中介对象来封装一系列对象之间的交互,使各对象不需要显式地相互引用,从而使其耦合松散,并可以独立地改变它们之间的交互。
生活类比:机场塔台,飞机之间不直接通信,全部通过塔台协调起降,把"网状"关系变成"星状"。
js
// 中介者
class ChatRoom {
register(user) {
user.chatRoom = this;
return user;
}
send(msg, from, to) {
to.receive(msg, from);
}
}
// 同事对象:只与中介者交互
class User {
constructor(name) {
this.name = name;
}
send(msg, to) {
this.chatRoom.send(msg, this, to);
}
receive(msg, from) {
console.log(`${from.name} 对 ${this.name} 说:${msg}`);
}
}
const room = new ChatRoom();
const zhang = room.register(new User("张三"));
const li = room.register(new User("李四"));
zhang.send("你好", li); // 张三 对 李四 说:你好中介者 vs 发布订阅:中介者封装并协调同事间的具体交互逻辑(知道谁和谁怎么交互);发布订阅的事件中心只是转发消息,不关心业务逻辑。
优缺点
- 优点:将网状的多对多关系简化为星状,降低对象间耦合。
- 缺点:中介者本身可能变得庞大复杂,成为难以维护的"上帝对象"。
前端体现
表单中多个字段的联动校验(由一个协调者统一管理)、复杂组件间通信的中转层都可用中介者。
访问者模式
核心概念
表示一个作用于某对象结构中各元素的操作。它使你可以在不改变各元素类的前提下,定义作用于这些元素的新操作,把"数据结构"与"作用于其上的操作"分离。
js
// 元素:接受访问者
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
accept(visitor) {
visitor.visit(this);
}
}
// 访问者:定义新操作,不修改元素类
class RaiseVisitor {
visit(employee) {
employee.salary *= 1.1; // 涨薪 10%
}
}
class ReportVisitor {
visit(employee) {
console.log(`${employee.name} 薪资:${employee.salary}`);
}
}
const emp = new Employee("张三", 10000);
emp.accept(new RaiseVisitor());
emp.accept(new ReportVisitor()); // 张三 薪资:11000优缺点
- 优点:新增操作只需新增访问者,符合开闭原则;把相关操作集中到访问者中。
- 缺点:新增元素类困难(每个访问者都要改);破坏元素封装。
前端体现
AST(抽象语法树)遍历——Babel/ESLint 插件通过 visitor 对不同节点类型执行不同处理,是访问者模式的经典应用。
解释器模式
核心概念
给定一个语言,定义它的文法的一种表示,并定义一个解释器,用来解释该语言中的句子。适用于需要解释执行特定"小型语言"或规则表达式的场景。
js
// 简单的加减法表达式解释器
class Interpreter {
constructor(expression) {
this.expression = expression; // 如 "3 + 5 - 2"
}
interpret() {
const tokens = this.expression.split(" ");
let result = Number(tokens[0]);
for (let i = 1; i < tokens.length; i += 2) {
const op = tokens[i];
const num = Number(tokens[i + 1]);
if (op === "+") result += num;
if (op === "-") result -= num;
}
return result;
}
}
console.log(new Interpreter("3 + 5 - 2").interpret()); // 6优缺点
- 优点:文法易于扩展和修改,规则清晰。
- 缺点:文法复杂时类数量爆炸、难以维护,执行效率较低。
前端体现
模板引擎的语法解析、正则表达式引擎、简单公式/搜索语法(如 age > 18 && vip)的解析都属于解释器模式。
备忘录模式
核心概念
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后可以将对象恢复到原先保存的状态。
js
// 备忘录:保存快照
class Memento {
constructor(state) {
this.state = state;
}
}
// 原发器:创建/恢复快照
class Editor {
constructor() {
this.content = "";
}
type(text) {
this.content += text;
}
save() {
return new Memento(this.content);
}
restore(memento) {
this.content = memento.state;
}
}
// 管理者:保存历史快照栈
class History {
constructor() {
this.stack = [];
}
push(memento) {
this.stack.push(memento);
}
pop() {
return this.stack.pop();
}
}
const editor = new Editor();
const history = new History();
editor.type("Hello");
history.push(editor.save()); // 保存快照
editor.type(" World");
editor.restore(history.pop()); // 撤销到 Hello
console.log(editor.content); // Hello优缺点
- 优点:提供状态恢复能力(撤销/回滚),不破坏对象封装。
- 缺点:保存大量快照会占用较多内存。
前端体现
编辑器撤销/重做、表单草稿保存与恢复、游戏存档、状态管理的时间旅行调试都是备忘录模式。
设计模式选型速查
一句话记忆
设计模式的本质是面向接口/抽象编程,核心目的是高内聚、低耦合、易扩展。不要为了用模式而用模式——先有痛点,再选模式。
三大分类速记
| 分类 | 目的 | 包含模式 |
|---|---|---|
| 创建型 | 解决"对象怎么创建" | 工厂、抽象工厂、单例、原型、建造者 |
| 结构型 | 解决"对象怎么组合" | 适配器、装饰器、代理、外观、桥接、组合、享元 |
| 行为型 | 解决"对象怎么协作" | 观察者、发布订阅、迭代器、状态、策略、模板方法、责任链、命令、中介者、访问者、解释器、备忘录 |
按需求场景选型
| 你的需求 | 推荐模式 |
|---|---|
| 全局唯一实例(如全局弹窗、Store) | 单例 |
| 屏蔽复杂子系统,提供简单接口 | 外观 |
| 兼容不兼容的接口(如老 API 适配新组件) | 适配器 |
| 不改原对象,动态增强功能 | 装饰器 |
| 控制/拦截对对象的访问(缓存、校验、懒加载) | 代理 |
| 状态变化通知多方更新 | 观察者 / 发布订阅 |
| 消除大量 if/else 分支(算法可切换) | 策略 |
| 消除分支且状态间有流转 | 状态 |
| 请求需要多级处理、层层传递 | 责任链 |
| 需要撤销/重做能力 | 命令 / 备忘录 |
| 统一流程、局部步骤可变 | 模板方法 |
| 多对象网状交互需要解耦 | 中介者 |
| 遍历集合而不暴露内部结构 | 迭代器 |
| 遍历树/AST 并对节点做不同处理 | 访问者 |
易混淆模式对比
| 对比项 | 区别要点 |
|---|---|
| 代理 vs 装饰器 | 代理控制访问(接口不变);装饰器增强功能(接口不变,可叠加) |
| 适配器 vs 代理/装饰器 | 适配器改变接口以兼容;后两者保持接口 |
| 观察者 vs 发布订阅 | 观察者目标直接通知;发布订阅通过事件中心解耦 |
| 策略 vs 状态 | 策略平行独立由外部选择;状态间有流转由内部切换 |
| 中介者 vs 发布订阅 | 中介者协调具体交互逻辑;事件中心只转发消息 |
实践建议
优先掌握前端高频模式:单例、工厂、观察者/发布订阅、代理、策略、装饰器、责任链。其余模式了解思想即可,遇到对应痛点时再深入。