前端9种设计模式
1. 设计模式类型
2. 结构型模式
3. 创建型模式
4. 行为型模式
前端需要了解的9种设计模式 - 知乎 (zhihu.com)
1. 设计模式可以分为三大类:
2. 结构型模式
外观模式
外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已。
比如,我们可以应用外观模式封装一个统一的DOM元素事件绑定/取消方法,用于兼容不同版本的浏览器和更方便的调用:
// 绑定事件
function addEvent(element, event, handler) {
if (element.addEventListener) {
element.addEventListener(event, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
} else {
element['on' + event] = fn;
}
}
代理模式
首先,一切皆可代理。现实世界中买房有中介、打官司有律师、投资有经纪人,他们都是代理,由他们帮你处理由于你缺少时间或者专业技能而无法完成的事务。类比到计算机领域,代理也是一样的作用,当访问一个对象本身的代价太高(比如太占内存、初始化时间太长等)或者需要增加额外的逻辑又不修改对象本身时便可以使用代理。ES6中也增加了 Proxy 的功能。
归纳一下,代理模式可以解决以下的问题:
要实现代理模式需要三部分:
比如有一个股票价格查询接口,调用这个接口需要比较久的时间(用 setTimeout 模拟2s的调用时间)
我们不希望每次都去请求远程接口,而是增加缓存机制,当有缓存的时候就直接从缓存中获取,否则再去请求远程接口。我们可以通过一个proxy来实现
注意,Proxy需要和真实对象一样实现 getValue() 方法,getValue()就属于 Subject 接口。
3. 创建型模式
工厂模式
工厂可以看成是一个制造其他对象的对象,制造出的对象也会随着传入工厂对象参数的不同而有所区别。
什么场景适合应用工厂模式而不是直接 new 一个对象呢?当构造函数过多不方便管理,且需要创建的对象之间存在某些关联(有同一个父类、实现同一个接口等)时,不妨使用工厂模式。工厂模式提供一种集中化、统一化的方式,避免了分散创建对象导致的代码重复、灵活性差的问题。
// 汽车构造函数
function HondaCar(color) {
this.color = color;
this.brand = 'Honda';
}
// 汽车构造函数
function BMWCar(color) {
this.color = color;
this.brand = 'BMW';
}
// 汽车品牌枚举
const BRANDS = {
honda: 2,
bmw: 3
}
/**
* 汽车工厂
*/
function CarFactory() {
this.create = function (brand, color) {
switch (brand) {
case BRANDS.honda:
return new HondaCar(color);
case BRANDS.bmw:
return new BMWCar(color);
default:
break;
}
}
}
const carFactory = new CarFactory();
const cars = [];
cars.push(carFactory.create(BRANDS.honda, 'grey'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));
使用工厂模式之后,不再需要重复引入一个个构造函数,只需要引入工厂对象就可以方便的创建各类对象。
单例模式
顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。
实现单例模式需要解决以下几个问题:
我们一般通过实现以下两点来解决上述问题:
Javascript中单例模式可以通过以下方式实现:
// 单例构造器
const FooServiceSingleton = (function () {
// 隐藏的Class的构造函数
function FooService() {}
// 未初始化的单例对象
let fooService;
return {
// 创建/获取单例对象的函数
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
实现的关键点有:1. 使用 IIFE创建局部作用域并即时执行;2. getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
3. 行为型模式
策略模式
策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法。
最常见的使用策略模式的场景如登录鉴权,鉴权算法取决于用户的登录方式是手机、邮箱或者第三方的微信登录等等,而且登录方式也只有在运行时才能获取,获取到登录方式后再动态的配置鉴权策略。所有这些策略应该实现统一的接口,或者说有统一的行为模式。Node 生态里著名的鉴权库 Passport.js API的设计就应用了策略模式
迭代器模式
迭代器的接口都应该是一样的,都需要遵循 迭代器协议。
迭代器模式解决了以下问题:
一个迭代器通常需要实现以下接口:
为Javascript的数组实现一个迭代器可以这么写:
const item = [1, 'red', false, 3.14];
function Iterator(items) {
this.items = items;
this.index = 0;
}
Iterator.prototype = {
hasNext: function () {
return this.index < this.items.length;
},
next: function () {
return this.items[this.index++];
}
}
const iterator = new Iterator(item);
while(iterator.hasNext()){
console.log(iterator.next());
}
ES6提供了更简单的迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。
next () {
return { value: <number>, done: <boolean> }
}
比如我们实现一个 Range 类用于在某个数字区间进行迭代:
function Range(start, end) {
return {
[Symbol.iterator]: function () {
return {
next() {
if (start < end) {
return { value: start++, done: false };
}
return { done: true, value: end };
}
}
}
}
}
观察者模式
观察者模式又称发布订阅模式(Publish/Subscribe Pattern)
观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。
观察者模式中Subject对象一般需要实现以下API:
用JavaScript手动实现观察者模式:
// 被观察者
function Subject() {
this.observers = [];
}
Subject.prototype = {
// 订阅
subscribe: function (observer) {
this.observers.push(observer);
},
// 取消订阅
unsubscribe: function (observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;
})
},
// 事件触发
fire: function () {
this.observers.forEach(observer => {
observer.call();
});
}
}
const subject = new Subject();
function observer1() {
console.log('Observer 1 Firing!');
}
function observer2() {
console.log('Observer 2 Firing!');
}
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire(); // Observer 1 Firing!
// Observer 2 Firing!
中介者模式
在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的方式,使得这些对象不必直接相互作用,而是由中介者协调它们之间的交互,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。
中介者模式和观察者模式有一定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是处理同级对象之间的交互,而观察者模式是处理Observer和Subject之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能直接交流,而是要通过中介去筛选匹配再决定谁和谁见面。中介者模式比较常见的应用比如聊天室,聊天室里面的人之间并不能直接对话,而是通过聊天室这一媒介进行转发。
一个简易的聊天室模型可以实现如下:
聊天室成员类:
function Member(name) {
this.name = name;
this.chatroom = null;
}
Member.prototype = {
// 发送消息
send: function (message, toMember) {
this.chatroom.send(message, this, toMember);
},
// 接收消息
receive: function (message, fromMember) {
console.log(`${fromMember.name} to ${this.name}: ${message}`);
}
}
聊天室类:
function Chatroom() {
this.members = {};
}
Chatroom.prototype = {
// 增加成员
addMember: function (member) {
this.members[member.name] = member;
member.chatroom = this;
},
// 发送消息
send: function (message, fromMember, toMember) {
toMember.receive(message, fromMember);
}
}
const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');
chatroom.addMember(bruce);
chatroom.addMember(frank);
bruce.send('Hey frank', frank);
访问者模式
访问者模式是一种将算法与对象结构分离的设计模式,通俗点讲就是:访问者模式让我们能够在不改变一个对象结构的前提下能够给该对象增加新的逻辑,新增的逻辑保存在一个独立的访问者对象中。访问者模式常用于拓展一些第三方的库和工具。
访问者模式的实现有以下几个要素:
补充
补充:前端最常见的设计模式集中在「创建型 / 结构型 / 行为型」三大类,本文已覆盖外观、代理、工厂、单例、策略、迭代器、观察者、中介者、访问者。下面补充未提及的几种。
其他常用模式:
- 装饰器模式(Decorator):动态给对象添加职责,例如 React HOC、Vue 插件、TS 装饰器。
- 适配器模式(Adapter):把不兼容的接口转换成期望的接口,例如旧 API 兼容层、Vue/React 适配同一组件库。
- 发布订阅模式(Pub/Sub):观察者模式的扩展,通过事件中心解耦发布者和订阅者,例如 Node EventEmitter、Vue EventBus。
- 职责链模式(Chain of Responsibility):把请求沿链传递,每个节点决定是否处理,例如 Koa / Express 中间件、Redux 中间件。
- 状态模式(State):把状态相关的行为封装到独立对象,状态变化时切换行为对象,例如 Promise 状态机、XState。
- 享元模式(Flyweight):通过共享细粒度对象减少内存占用,例如虚拟列表、图标库缓存。
实战建议:
- 不要为了「用设计模式」而设计模式;当一段逻辑被多次复制、或者扩展点逐渐增多时,再考虑重构。
- 前端很多框架本身已经实现了设计模式(Vue 响应式 = 观察者 + Proxy,Redux 中间件 = 职责链)。
- 学习模式时多看「适用场景 + 反模式」,比记类图更重要。



