Skip to content

设计模式 ​

导航目录 ​

一、设计原则篇

二、创建型模式

三、结构型模式

四、行为型模式

五、总结


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、ObserverPublisher、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 发布订阅中介者协调具体交互逻辑;事件中心只转发消息

实践建议

优先掌握前端高频模式:单例、工厂、观察者/发布订阅、代理、策略、装饰器、责任链。其余模式了解思想即可,遇到对应痛点时再深入。