接着之前的思路今天来介绍一下常用的设计模式有哪些
单例模式(Singleton Pattern)
又称为单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。
比如说常见的前端的 window、document、Redux数据仓库store等…这些都是很典型的单例模式。
再比如前端常用的jquery、axios对外暴露的都是全局唯一的一个实例。
通过静态属性创建单例
class Person{
static instance = null;
constructor(name){
if(Person.instance){
return Person.instance;
}
Person.instance = this;
this.name = name;
}
}
let zhangsan = new Person("张三")
let lisi = new Person("李四")
// 虽然示例话了两次,但返回的实际是全局内的同一个
console.log(zhangsan, lisi)
console.log(zhangsan===lisi);
通用单例
上面的方法可以,但不够通用。比如此时有一个Person, 一个Animal的话,那就需要在每个类中都定义这么个静态属性来判断。
此时我们就可以使用工厂模式来封装判断一下
class Person{
constructor(name){
this.name = name;
}
}
// 通用单例,通过闭包判断是否有已近有实例存在了
function createInstance(fn){
let instance;
return function(...args){
if(!instance){
instance = new fn(...args);
}
return instance
}
}
let singlePerson = createInstance(Perosn);
let zhansan = new singlePerson("张三");
let lisi = new singlePerson("李四");
console.log(zhangsan, lisi)
console.log(zhansan===lisi);
可以看到此时的效果是一样的,但这样更加的通用,我们吧单利的判断逻辑单独的抽离出去。正常需要使用单例的class只需要通过我们提供的函数包装一下即可。
工厂模式
工厂模式 (Factory Pattern),封装具体实例创建逻辑和过程,外部只需要根据不同条件返回不同的实例。
对象的创建和实现做对应的分离,解耦了实现和创建。
可以类比下取货是从工厂取货的,货物的实现是工厂里面的工人在另外一个地方(另外的函数/类/对象)中实现的
● 优点:实现代码复用性,封装良好,抽象逻辑;
● 缺点:增加了代码复杂程度;
比如下面两个Person和Animal两个类。两个类的方法/属性都在各自的类中
class Person {
constructor(name) {
this.name = name;
}
}
class Animal {
constructor(name) {
this.name = name;
}
}
可以用一个工厂类封装类实例的创建过程, 相当于用Factory来实例出对象,把上面功能的实现给抽离出来。
function Factory(name) {
//使用者只需要关注怎么创建/使用实例即可,具体的类的内部实现不关心
switch (name) {
case 'person':
return new Person();
break;
case 'animal':
return new Animal();
break;
default:
console.log("无...");
break;
}
}
const person = Factory("person");
const animal = Factory("animal");
console.log(person,animal);
装饰者模式
装饰者模式 (Decorator Pattern)使用一种更为灵活的方式来动态给一个对象/函数等添加额外信息/功能
● 扩展功能和继承类似
● 扩展不同类的功能,和原始类并无关联;
缺点:要装饰的对象不清楚
其实就是功能的扩展,以类举例,在类中可以使用extend来继承,extend很多情况下都是在基类中去扩展一些新的功能
而装饰着模式,都是拓展一些额外的功能。
比如看下面这个demo
class Person{
constructor(){
this.name = "张三";
}
skill(){
console.log("张三有技能.");
}
}
let zhansan = new Person();
zhansan.skill();
function level(){
console.log("软件开发架构师");
}
function years(){
console.log("工作了8年");
}
Function.prototype.Decorator = function(fn){
// 谁调用了这个Function那么this就指向谁
let _this = this;
// 返回一个函数(函数原型链上有Decorator)
// 这样后面就可以接着使用Decorator了,就形成了装饰者链
return function(){
// yase.release()这个方法要执行
_this();
// 同时也要拓展/执行传递进来的方法
fn();
}
}
zhansan.skill.Decorator(level)();
// 装饰者链
zhansan.skill.Decorator(level).Decorator(years)();
定义了一个Person的类,有技能。但是我们期望在有技能的基础上新增一些功能,比如工作的年限,以及当前的水平是如何。
我们就可以使用装饰着模式来增加Person的功能
最后一次打印的日志如下:
观察者模式
观察者模式 (Observer Pattern) 定义一个对象与其他对象之间的一种依赖关系,当对象发生某种变化的时候,依赖它的其它对象都会得到更新。
优点:
- 把一个对象和另一个对象关联在一起
- 可以惰性执行,比如回调函数,并不会立即执行,而是被观察的对象相应的事件触发之后才会执行
- 可以一对多的关系
意义:做解耦,把两个类,两个对象,两个模块通过事件管理,统一去管理事件,把事件解耦
这个在平时开发中其实就有很多的应用场景。比如下面其实就是把.box这个元素观察起来,如果这个元素被点击了,就会触发对应的回调
document.querySelector(".box").addEventListener("click",function(){
console.log("click1");
})
// 一对多的注册也ok
document.querySelector(".box").addEventListener("click",function(){
console.log("click1");
})
document.querySelector(".box").addEventListener("click",function(){
console.log("click2");
})
基本实现
// 先定义两个对象,将这两个对象关联起来
let obj1 = {
fn1(){
console.log("fn1更新");
}
}
let obj2 = {
fn2(){
console.log("fn2更新");
}
}
// 管理事件类
class MyEvent{
constructor(){
this.handles = {};
}
addEvent(eventName,fn){
// {myevent1:[fn1,fn2...],myevent2:[fn1,fn2...]}
if(typeof this.handles[eventName]==="undefined"){
// 第一次没有,先初始化为一个数组
this.handles[eventName] = [];
}
// 该事件对应的有一个数组事件
this.handles[eventName].push(fn);
}
trigger(eventName){
// 当前事件不在handles里面
if(!(eventName in this.handles)){
return ;
}
// 监听该事件的数组函数全部执行一遍
this.handles[eventName].forEach(fn=>{
fn();
})
}
}
// 使用
let eventObj = new MyEvent();
eventObj.addEvent("myevent",obj1.fn1);
eventObj.addEvent("myevent",obj2.fn2);
setTimeout(()=>{
eventObj.trigger("myevent")
},1000)
代理模式
代理模式为其他对象提供一种代理以控制对这个对象的访问,类似于生活中的中介。需要对原本代理的对象做一些控制,如果不做控制的话,那就没有多大意义了
应用
- proxy 服务器代理 转发请求 nginx
- ES6 的Proxy,可以代理对象,对原本的对象做一些控制(VUE3的核心)
Demo
我们可以代理图片的展示,以实现图片懒加载的效果
class CreateImage{
constructor(){
this.img = document.createElement("img");
document.body.appendChild(this.img);
}
setSrc(src){
this.img.src = src;
}
}
// 目标图片,
let src = "http2://x.x.xx.xxx";
// 代理图片的创建
function proxyImg(src){
let myImg = new CreateImage();
// 创建一个Img加载目标图片
let loadImg = new Image();
// 本地图片,加载快速
myImg.setSrc("./loading.gif");
// 创建的loadImg真实加载目标图片
loadImg.src = src;
// 目标图片加载到本地之后,再修改myImg的src
loadImg.onload = function(){
myImg.setSrc(src);
}
}
proxyImg(src);
适配器模式
两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
类似举个例子比如中国电压220v去欧洲不能正常充电,就需要一个转换器来做适配
我们在编码时最好不要过多的使用,过多的使用就会显得比较凌乱,要适度使用,使用好。
在前端axios兼容前端浏览器和node,实际上就是使用了适配器。里面有个模块专门做适配的
// 适配器模式
function getUsers(){
return [{
name:"yjian",
age:21
},{
name:"cyril",
age:26
}]
}
// 如果期望返回的: // [{yjian:21},{cyril,26}]
function Adaptor(users){
let arr = [];
for(let i=0;i<users.length;i++){
arr[users[i].name] = users[i].age;
}
return arr;
}
let res = Adaptor(getUsers());
console.log(res);
mixin 混入模式
也是为了扩展原有对象的功能。vue2里面用的比较多
// 混入模式
// 工程师
class FrontEngineer{
constructor(){
this.name = "工程师";
}
}
// 工程师的技能单独抽成一个类(除了前端工程师,其他工程师也是有技能的)
class Skills{
code(){
console.log("编写代码")
}
architect(){
console.log("技术架构能力");
}
http(){
console.log("了解http协议");
}
}
// 目标:实例化一个工程师,工程师有技能
// 可以使用集成 比如 Skill extend FrontEngineer.
// 那技能继承工程师,当前场景需求虽然满足,但会觉得非常奇怪,技能继承于职位。而且其他职位也有技能
// js语言里面没有多继承的方式,即一个类即继承FrontEngineer,又继承Skills
// 所以就需要这种混入模式
// let engineer = new FrontEngineer();
// 混入模式,思路是 Class其实都是函数,在原型链上找到技能的函数强行混到FrontEngineer的原型链即可
function mixin(receivingClass,givingClass){
// 后面的参数就是需要混入的函数名
if(typeof arguments[2] !== "undefined"){
for(let i=2;i<arguments.length;i++){
receivingClass.prototype[arguments[i]]= givingClass.prototype[arguments[i]];
}
}
}
// 把Skills中的code, architect, http技能给混入到FrontEngineer中去
mixin(FrontEngineer,Skills,"code","architect","http");
let engineer = new FrontEngineer();
console.log(engineer);
engineer.architect();
可以看到在engineer的原型链上已近混入了code、architect、http方法
享元模式
享元模式:运用共享技术来有效地支持对象的复用,以减少创建的对象的数量。
通过共享对象节约内存资源,提高性能和效率