设计模式

4/4/2023 javascript

设计模式都是面向对象的

工厂模式

工厂模式就是做一个对象创建的封装,并将创建的对象return出去

function newObj(name, age) {
    var o = new Object();
    o.name = name
    o.age = age
    return o
}
var obj = newObj()

单例模式

只允许存在一个实例的模式


实现方式:使用一个变量储存实例对象(值为null/undefined。进行类实例化时,判断实例对象是否存在,存在则返回该实例,不存在则创建实例后返回。多次调用实例方法,返回同一个实例对象

let Singleton = function(name) {
    this.name = name;
    this.instance = null;
}

Singleton.prototype.getName = function() {
    console.log(this.name);
}

Singleton.getInstance = function(name) {
    if (this.instance) {
        return this.instance;
    }
    return this.instance = new Singleton(name);
}

let Winner = Singleton.getInstance('Winner');
let Looser = Singleton.getInstance('Looser');

console.log(Winner === Looser); // true
console.log(Winner.getName());  // 'Winner'
console.log(Looser.getName());  // 'Winner'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

惰性单例模式

出现惰性单例模式的原因:

需要时才创建实例对象。对于懒加载的优化,想必前端并不陌生。惰性加载也是解决按需加载的问题

适用场景:页面弹框提示,多次调用,都只有一个弹窗对象,只是展示的信息不一样

例如:页面存在一个模态框的时候,只有用户点击的时候才会创建,并保证全局弹窗只有一个

可以先创建一个通用的获取对象的方法,如下

const getSingle = (fn) => {
  let result;
  return function () {
    return result || result= fn.apply(this, arguments);
  }
}

创建弹窗的代码如下

const createLoginLayer = function () {
  var div = document.createElement("div");
  div.innerHTML = "我是浮窗";
  div.style.display = "none";
  document.body.appendChild(div);
  return div;
}
const createSingleLoginLayer = getSingle(createLoginLayer);

document.getElementById("login")?.onclick = function () {
  var loginLayer = createSingleLoginLayer();
  loginLayer.style.display = "block";
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

还可以用到什么场景呢?

1.引用第三方库(多次引用只会使用一个库引用,如jQuery)

2.弹窗(登录酷,信息提示框)

3.购物狂(一个用户只有一个购物车)

4.全局管理store(vuex\redux)

项目中引入第三方库时,重复多次加载库文件时,全局只会实例化一个库对象,如 jQuerylodashmoment ..., 其实它们的实现理念也是单例模式应用的一种:

if(!window.libs) {
    return;
} else {
    //初始化
    window.libs = "...";
}

# 优缺点

  • 优点:适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用。
  • 缺点:不适用动态扩展对象,或需创建多个相似对象的场景。

策略模式

策略模式指的是定义一系列的算法,把他们一个个封装起来,目的是将算法的使用和算法的实现分离开。

一个基于策略模式的程序至少由两部分组成:

  1. 策略类,策略类具体封装了具体的算法,并负责具体的计算过程
  2. 环境类Context,Context接受客户的请求,随后把请求委托给另一个策略类

使用

举个例子:生活中有很多种场景,我们出门旅游,可能有多种交通路线,轮船是200元,A类路线是高铁,是轮船的两倍;B类是飞机,是轮船的三倍C类是轮船,可以根据自己资金以及自身情况综合考虑。那我们用最简单的代码实现下

var calculatePrice = function(price, type) {
    if (type === "A") {
        return price*2
    }
    if (type === "B") {
        return price*3
    }
    if (type === "C") {
        return price
    }    
}
// 调用如下
console.log(calculatePrice(300, B))

从上述可有看到,函数内部包含过多if...else,并且后续改正的时候,需要在函数内部添加逻辑,违反了开放封闭原则

而如果使用策略模式,就是先定义一系列算法,把它们一个个封装起来,将不变的部分和变化的部分隔开,如下:

var obj = {
    "A": function(price) {
        return price*2        
    },
    "B": function(price) {
        return price*3        
    },
    "C": function(price) {
        return price        
    },
}

var calculatePrice = function(price, type) {
    return obj[type](level)
}
// 调用如下
console.log(calculatePrice(300, B))

上述代码中obj代表的是策略,calculatePrice代表的是上下通信context

应用场景

策略模式的优点如下

  • 策略模式利用组合、委托等技术和思想,有效的避免了很多if else
  • 策略模式提供了开放-封闭原则,使代码更容易理解和扩展
  • 策略模式中的代码可以服用

策略模式不仅仅可以封装算法,在实际开发中,也可以封装一系列的业务规则

代理模式

代理模式是为一个对象提供一个代用品或者占位符,以便控制对他的访问。

在生活中,代理模式的场景非常常见,例如,现在我们有租房、买房的需求,更多的去找我爱我家这种中介,此时中介起到的就是代理作用。

常用的代理模式有

  • 缓存代理
  • 虚拟代理

缓存代理

缓存代理可以为一些开销大的运算提供暂时的存储,在下次运算时,如果传递进来的参数和之前的一样,可以直接返回之前的存储结果

如实现一个求积乘的函数,如下:

var mult = function() {
    var a = 1;
    for (var i=0,l=arguments.length; i<l;i++) {
        a = a*arguments[i]
    }
    return a;
}
1
2
3
4
5
6
7

加入缓存代理代码

var proxyMult = function() {
    var cache = {};
    return function() {
        var args = Array.prototype.join.call(arguments, ",");
        if (args in cache) {
            return cache[args]
        }
        return (cache[args] = mult.apply(this, arguments))
    }
}
proxyMult(1, 2, 3, 4); // 24
proxyMult(1, 2, 3, 4); // 24
1
2
3
4
5
6
7
8
9
10
11
12

当第二次调用 proxyMult(1, 2, 3, 4) 时,本体 mult 函数并没有被计算,proxyMult 直接返回了之前缓存好的计算结果

# 虚拟代理

虚拟代理是把一些开销很大的对象延迟到真正需要他的时候再去创建

常见的就是图片预加载功能

未使用代理时如下代码

let MyImage = (function(){
    let imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    // 创建一个Image对象,用于加载需要设置的图片
    let img = new Image;

    img.onload = function(){
        // 监听到图片加载完成后,设置src为加载完成后的图片
        imgNode.src = img.src;
    };

    return {
        setSrc: function( src ){
            // 设置图片的时候,设置为默认的loading图
            imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif';
            // 把真正需要设置的图片传给Image对象的src属性
            img.src = src;
        }
    }
})();

MyImage.setSrc( 'https://xxx.jpg' );

MyImage对象除了负责给img节点设置src外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则

上述过程loding则是耦合进MyImage对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在MyImage对象里面改动代码

使用代理之后,如下代码

// 图片本地对象,负责往页面中创建一个img标签,并且提供一个对外的setSrc接口
let myImage = (function() {
    let imgNode = document.createElement( 'img' );
    document.body.appendChild( imgNode );
    return {
        // setSrc接口,外界调用这个接口,便可以给该img标签设置src属性
        setSrc: function( src ){
            imgNode.src = src; 
        }
    }
})

// 代理对象,负责图片预加载功能
let proxyImage = (function() {
// 创建一个Image对象,用于加载需要设置的图片
    let img = new Image;

    img.onload = function(){
        // 监听到图片加载完成后,给被代理的图片本地对象设置src为加载完成后的图片
        imgNode.src = myImage.setSrc(this.src);
    };
    return {
        setSrc: function( src ){
            // 设置图片时,在图片未被真正加载好时,以这张图作为loading,提示用户图片正在加载
            myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' );
            img.src = src;
        }
    }
})

proxyImage.setSrc( 'https://xxx.jpg' );

使用代理模式后,图片本地对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口;

代理对象负责在图片未加载完成之前,引入预加载的loading图,负责了图片预加载的功能

上述并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为

并且上述代理模式可以发现,代理和本体接口的一致性,如果有一天不需要预加载,那么就不需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了 setSrc 方法

应用场景

现在很多前端框架以及状态管理框架都使用代理模式,监听变量的变化

比如我们项目中经常用的axios的interceptor拦截器,使用拦截器可以提前对请求前的数据以及服务器返回的数据进行一些预处理

# 发布订阅、观察者模式

观察者模式

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新

例如生活中,我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸

报社和订报纸的客户就形成了一对多的依赖关系

发布订阅模式

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在

就是发布和订阅由中间的桥梁进行管理,两者互不关联

区别

  • 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
  • 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)

参考文献

https://vue3js.cn/interview/ (opens new window)