本文主要介绍前端框架中的设计模式,以部分设计模式的使用案例为背景,从创建型、结构型、行为型模式三大方面简洁地了解前端框架的搭建与应用。
一、 设计模式概览
设计模式是对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,当然每种设计模式都有每种语言中的具体实现方式。
学习设计模式更多的是理解各种模式的内在思想和解决的问题,毕竟这是前人无数经验总结成的最佳实践,而代码实现则是对加深理解的辅助。
设计模式可以分为三大类:
- 结构型模式(Structural Patterns): 通过识别系统中组件间的简单关系来简化系统的设计。
- 创建型模式(Creational Patterns): 处理对象的创建,根据实际情况使用合适的方式创建对象。常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
- 行为型模式(Behavioral Patterns): 用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性。
二、 前端框架中的设计模式
2.1 创建型模式
顾名思义,这些模式都是用来创建实例对象的。
2.1.1 工厂模式
简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。
上图为例,我们构造一个简单的汽车工厂来生产汽车:
js
// 汽车构造函数
function SuzukiCar(color) {
this.color = color;
this.brand = 'Suzuki';
}
// 汽车构造函数
function HondaCar(color) {
this.color = color;
this.brand = 'Honda';
}
// 汽车构造函数
function BMWCar(color) {
this.color = color;
this.brand = 'BMW';
}
// 汽车品牌枚举
const BRANDS = {
suzuki: 1,
honda: 2,
bmw: 3
}
/**
* 汽车工厂
*/
function CarFactory() {
this.create = (brand, color)=> {
switch (brand) {
case BRANDS.suzuki:
return new SuzukiCar(color);
case BRANDS.honda:
return new HondaCar(color);
case BRANDS.bmw:
return new BMWCar(color);
default:
break;
}
}
}
使用一下我们的工厂:
js
const carFactory = new CarFactory();
const cars = [];
cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
cars.push(carFactory.create(BRANDS.honda, 'grey'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));
function sayHello() {
console.log(`Hello, I am a ${this.color} ${this.brand} car`);
}
for (const car of cars) {
sayHello.call(car);
}
输出结果:
js
Hello, I am a brown Suzuki car
Hello, I am a grey Honda car
Hello, I am a red BMW car
使用工厂模式之后,不再需要重复引入一个个构造函数,只需要引入工厂对象就可以方便的创建各类对象。
2.1.2 单例模式
- 单: 指的是一个
- 例: 指的是创建的实例
- 单例: 指的是创建的总是同一个实例,也就是使用类创建的实例始终是相同的
先看下面的一段代码:
js
class Person{
constructor(){}
}
let p1 = new Person();
let p2 = new Person();
console.log(p1===p2) //false
上面这段代码,定义了一个Person类,通过这个类创建了两个实例,我们可以看到最终这两个实例是不相等的。也就是说,通过同一个类得到的实例不是同一个(这本就是理所应当),但是如果我们想始终得到的是同一个实例,那么这就是单例模式。
那么下面就该介绍如何实现单例模式: 想要实现单例模式,我们需要注意两点:
- 需要使用 return。 使用 new 的时候如果没有手动设置 return,那么会默认返回 this。但是,我们这里要使得每次返回的实例相同,也就是需要手动控制创建的对象,因此这里需要使用 return。
- 需要每次 return 的是同一个对象。 也就是说实际上在第一次实例的时候,需要把这个实例保存起来。再下一个实例的时候,直接 return 这个保存的实例。因此,这里需要用到闭包了。
js
const Person = (function(){
let instance = null;
return class{
constructor(){
if(!instance){
//第一次创建实例,那么需要把实例保存
instance = this;
}else{
return instance;
}
}
}
})()
let p3 = new Person();
let p4 = new Person();
console.log(p3===p4) //true
从上面的代码中,我们可以看到在闭包中,使用 instance
变量来保存创建的实例,每次返回的都是第一次创建的实例。这样的话就实现了无论创建多少次,创建的都是同一个实例,这就是单例模式
。
2.1.3 原型模式
通俗点讲就是创建一个共享的原型 ,并通过拷贝 这些原型创建新的对象。
在我看来,其实原型模式就是指定新创建对象的模型,更通俗一点来说就是我想要新创建的对象的原型是我指定的对象。
最简单的原型模式的实现就是通过 Object.create()。Object.create(),会使用现有的对象来提供新创建的对象的__proto__
。
例如下方代码:
js
let person = {
name:'hello',
age:24
}
let anotherPerson = Object.create(person);
console.log(anotherPerson.__proto__) //{name: "hello", age: 24}
anotherPerson.name = 'world'; //可以修改属性
anotherPerson.job = 'teacher';
如果想要自己实现原型模式,而不是使用封装好的 Object.create() 函数,那么可以使用原型继承来实现。
原型模式就是创建一个指定原型的对象 。如果我们需要重复创建某个对象,那么就可以使用原型模式来实现。
2.2 结构型模式
2.2.1 装饰器模式
装饰器模式: 为对象添加新功能,不改变其原有的结构和功能。
适配器模式
是原有的不能用了,要重新封装接口。装饰器模式是原有的还能用,但是需要新增一些东西来完善这个功能。比如手机壳,手机本身的功能不受影响,手机壳就是手机的装饰器模式。
js
class Circle {
draw() {
console.log('画一个圆形');
}
}
class Decorator {
constructor(circle) {
this.circle = circle;
}
draw() {
this.circle.draw();
this.setRedBorder(circle);
}
setRedBorder(circle) {
console.log('设置红色边框')
}
}
// 测试
let circle = new Circle();
let client = new Decorator(circle);
client.draw();
输出结果:
js
画一个圆形
设置红色边框
验证是否是一个真正的装饰器模式需要验证以下几点:
js
1.将现有对戏那个和装饰器进行分离,两者独立存在
2.符合开放封闭原则
2.2.2 适配器模式
适配器模式: 旧接口格式和使用者不兼容,中间加一个适配转换接口。
比如国外的插座跟国内的插座不一样,我们需要买个转换器去兼容。
代码如下:
js
class Adaptee {
specificRequest() {
return '德国标准的插头';
}
}
class Target {
constructor() {
this.adaptee = new Adaptee();
}
request() {
let info = this.adaptee.specificRequest();
return `${info} -> 转换器 -> 中国标准的插头`
}
}
// 测试
let client = new Target();
client.request();
结果:
js
德国标准的插头 -> 转换器 -> 中国标准的插头
场景上可封装旧接口:
js
// 自己封装的ajax,使用方式如下:
ajax({
url: '/getData',
type: 'Post',
dataType: 'json',
data: {
id: '123'
}
}).done(function(){
})
// 但因为历史原因,代码中全都是:
// $.ajax({...})
此时需要一个适配器:
js
// 做一层适配器
var $ = {
ajax: function (options) {
return ajax(options)
}
}
2.2.3 代理模式
代理模式: 使用者无权访问目标对象,中间加代理,通过代理 做授权和控制。
明星经纪人:比如有个演出,要请明星,要先联系经纪人。 或者理解为:为一个对象提供一个代用品或者占位符,以便控制对它的访问。例如图片懒加载、中介等。
2.3 行为型模式
2.3.1 策略模式
策略模式是一种简单却常用的设计模式,它的应用场景非常广泛。
策略模式由两部分构成:
- 封装不同策略的策略组
- Context
通过组合和委托来让 Context 拥有执行策略的能力,从而实现可复用、可扩展和可维护,并且避免大量复制粘贴的工作。
2.3.2 观察者模式
观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一 或一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。典型代表vue/react等。
使用观察者模式的优点:
- 支持简单的广播通信,自动通知所有已经订阅过的对象
- 目标对象与观察者存在的是动态关联,增加了灵活性
- 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用
2.3.3 迭代器模式
ES6 中的迭代器 Iterator
相信大家都不陌生,迭代器用于遍历容器(集合)并访问容器中的元素,而且无论容器的数据结构是什么(Array、Set、Map等),迭代器的接口都应该是一样的,都需要遵循迭代器协议。
迭代器模式解决了以下问题:
- 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构
- 提供遍历容器(集合)的能力而无需改变容器的接口
一个迭代器通常需要实现以下接口:
hasNext()
:判断迭代是否结束,返回Booleannext()
:查找并返回下一个元素
2.3.4 状态模式
状态模式:一个对象有状态变化,每次状态变化都会触发一个逻辑,不能总是用 if...else
来控制。
设计原则验证
- 将状态对象和主体对象分离,状态的变化逻辑单独处理
- 符合开放封闭原则