看完吃透JS设计模式💐

184 阅读12分钟

哈喽,大家好!我是奶茶不加糖。一个喜欢喝奶茶的前端攻城狮 ( 哈哈,今天也是努力搬砖的一天(* ̄︶ ̄) )😁😁😁

前言

🙌 有很多小伙伴在学习js的过程中,都会觉得设计模式是比较难的,但是是我们高级进阶的必经之路,也是为了让我们可以写出更好更优更快的代码,所以下面我总结了几种常用的设计模式,一起看看叭叭叭💪💪💪

什么是设计模式

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模式

学习设计模式,有助于写出可复用和可维护性高的程序
设计模式的原则是“找出 程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。
不过要注意,使用不当的话,可能会事倍功半。

一、工厂模式

工厂模式:工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。
简单的工厂模式可以理解为解决多个相似的问题;这也是她的优点;

比如如下代码:通过工厂函数获取想要的内容

function creatPerson(name) {
  // 创造出想要的对象
  var obj = {};
  // 给对象添加必要的数据
  obj.name = name;
  // 把创建好的对象返回出去
      return obj;
 }

:简化创造对象的操作,只需要调用函数就可以获取对象
:无法判断出对象的归属

var obj1 = creatPerson('宁哥');
console.log(obj1 instanceof creatPerson);// --> false
console.log(obj1 instanceof Object);// --> true

二、单例模式

单例模式:通过单例创建出来的对象,只初始化(实例化)一次,以后调用创建方法
获取到的对象永远是第一次调用所创建出来的对象

function CreatePerson(name){
   this.name = name;
}
CreatePerson.prototype.sayHi = function(){
   console.log("hello");
}

1、不使用闭包,在函数内部调用内部函数

这样的做法不合理,因为这样还是会每次调用这个函数创建出好多不同的对象

var person = null;//把这个写成全局中,对代码结构不太好
function SingalPerson(){
  var person = null;
  function aaa(name){
    if(!person){
       person = new CreatePerson(name);
     }
   }
  aaa(name)
  console.log(person);
  //这样子return出去后函数里面就没有了person了,又还需要创建
  return person;
  console.log(person);
}
var a = SingalPerson();
console.log(a);

2、用闭包的方式是 person 被 return 出去后不会被销毁,一直存在内存中

单例创造对象的函数

function SingalPerson(){
  var person = null;
  return function(name){
      if(!person){
          person = new CreatePerson(name);
      }
      return person;
     }
}

虽然可以只调用一步SingalPerson();就可以实现单例效果,但是如果两个 对象分别是在函数里面创建,就需要在各自的函数里调用两次SingalPerson; 当然也可以把调用SingalPerson()的表达式写在全局中,这样每次创建对象 都可以来调用存贮SingalPerson()结果的变量,但是这样就把单例操作分 成了两部分,对程序的逻辑和结构方面不太好

var a = SingalPerson();
var obj1 = a("haha");
console.log(obj1);
//这样就可以看到单例的效果了
var obj3 = a("lala");
console.log(obj3);
//如果执行两次SingalPerson()就会创建两个不同的对象
//var b = SingalPerson();
//var obj2 = b("lala");
//console.log(obj2);

3、使用闭包和立即执行函数实现

要把最外面的函数只执行一遍,就可以使用到自调用,这样就不用在外面再去定义 执行全局的函数,当程序执行到这行代码时就立刻执行这个函数,再用一个变量来 保存这个函数的结果,然后可以在外面就可以调用这个变量了,因为外面的那个函 数返回的就是我们不断执行的内部函数

var aa = (function SingalPerson(){
  var person = null;
  return function(name){
    if(!person){
       person = new CreatePerson(name);
    }
    return person;
  }
})();
var obj1 = aa("haha");
console.log(obj1);
var obj2 = aa("lala");
console.log(obj2);

通过上面三种方式的说明,单例模式还是使用闭包和立即执行函数是最优的方式

4、单例模式的实例

单例常用场景

只需要生成一个唯一对象的时候,比如说页面登录框,只可能有一个登录框,那么你就可以用单例的思想去实现他,当然你不用单例的思想实现也行,那带来的结果可能就是你每次要显示登陆框的时候都要重新生成一个登陆框并显示(耗费性能),或者是不小心显示出了两个登录框(等着吃经理的 40 米长刀吧),嘻嘻嘻。

单例模式实现模态窗口

var creatDiv = (function(){
var div = null;
// 向外返回的函数是真正用来创建标签、拼接标签的操作
   return function() {
    if (!div) { // 判断标签是否存在,不存在就创建,存在就直接返回这个标签
      div = document.createElement('div');
      div.setAttribute('style', 'width: 100px;
      height:100px; border:1px solid black');
      document.body.appendChild(div);
    }
      return div;
}
})();
// 页面加载完毕后添加点击事件
window.onload = function () {
  document.getElementById('btn').onclick = function () {
      var divElement = creatDiv();
      divElement.style.display = 'block';
      divElement.onclick = function() {
        document.body.removeChild(divElement);
        divElement.style.display = 'none';
      }
  }
}

三、代理模式

代理是一个对象,它可以用来控制对本体对象的访问,它与本体对象实现了同样的接口,代理对象会把所有的调用方法传递给本体对象的;代理模式最基本的形式是对访问进行控制,而本体对象则负责执行所分派的那个对象的函数或者类,简单的来讲本地对象注重的去执行页面上的代码,代理则控制本地对象何时被实例化,何时被使用;我们在上面的单体模式中使用过一些代理模式,就是使用代理模式实现单体模式的实例化,其他的事情就交给本体对象去处理;

代理两个优点:

  • 代理对象可以代替本体被实例化,并使其可以被远程访问;
  • 它还可以把本体实例化推迟到真正需要的时候;对于实例化比较费时的本体对象,或者因为尺寸比较大以至于不用时不适于保存在内存中的本体,我们可以推迟实例化该对象

第一种方案: 不使用代理的预加载图片函数如下

// 不使用代理的预加载图片函数如下
var myImage = (function(){
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  var img = new Image();
  img.onload = function(){
      imgNode.src = this.src;
  };
  return {
      setSrc: function(src) {
      imgNode.src = "http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif";
      img.src = src;
      }
  }
})();

调用方式
myImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");

第二种方案: 使用代理模式来编写预加载图片的代码如下:

var myImage = (function(){
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
   })();
// 代理模式
var ProxyImage = (function(){
    var img = new Image();
    img.onload = function(){
        myImage.setSrc(this.src);
    };
    return {
        setSrc: function(src) {
            myImage.setSrc("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
            img.src = src;
        }
    }
})();

调用方式
ProxyImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");

第一种方案是使用一般的编码方式实现图片的预加载技术,首先创建imgNode元素,然后调用myImage.setSrc该方法的时候,先给图片一个预加载图片,当图片加载完的时候,再给img元素赋值,第二种方案是使用代理模式来实现的,myImage 函数只负责创建img元素,代理函数ProxyImage 负责给图片设置loading图片,当图片真正加载完后的话,调用myImage中的myImage.setSrc方法设置图片的路径;

他们之间的优缺点如下:

  • 第一种方案一般的方法代码的耦合性太高,一个函数内负责做了几件事情,比如创建 img 元素,和实现给未加载图片完成之前设置loading加载状态等多项事情,未满足面向对象设计原则中单一职责原则;并且当某个时候不需要代理的时候,需要从myImage 函数内把代码删掉,这样代码耦合性太高。
  • 第二种方案使用代理模式,其中 myImage 函数只负责做一件事,创建img元素加入到页面中,其中的加载loading图片交给代理函数ProxyImage 去做,当图片加载成功后,代理函数ProxyImage 会通知及执行myImage 函数的方法,同时当以后不需要代理对象的话,我们直接可以调用本体对象的方法即可;

从上面代理模式我们可以看到,代理模式和本体对象中有相同的方法setSrc,这样设置的话有如下2个优点:

  • 用户可以放心地请求代理,他们只关心是否能得到想要的结果。假如我门不需要代理对象的话,直接可以换成本体对象调用该方法即可。
  • 在任何使用本体对象的地方都可以替换成使用代理。

四、观察者模式

观察者模式:发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

发布订阅模式的优点:

  • 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
  • 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变;

发布订阅模式的缺点:

创建订阅者需要消耗一定的时间和内存。
虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等。

  • 首先要想好谁是发布者(比如上面的卖家)。
  • 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(买家收藏了卖家的店铺,卖家通过收藏了该店铺的一个列表名单)。
  • 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。

我们还可以在回调函数里面添加一点参数

var saleHouse = {};//发布者
saleHouse.clientlist = [];//客户的列表
saleHouse.listen = function(client){//注册被通知的客户,放入列表中
// this.clientlist.push(client);//
console.log(this.clientlist)//[]就是上面的saleHouse.clientlist
    saleHouse.clientlist.push(client)
}
saleHouse.trigger = function(){//发通知
for(var i = 0,s;s = this.clientlist[i++];){//跟我们平时写方法相同
    s.apply(this,arguments);//调用函数,有参数的情况下用apply
}
}
saleHouse.listen(function(position,price){
   console.log('地点:'+position+"价格:"+price)
})
   saleHouse.trigger('前海湾','120000')

五、适配器模式

适配器模式:将一个对象或者类的接口翻译成某个指定的系统可以使用的另外一个接口.

适配器基本上允许本来由于接口不兼容而不能一起正常工作的对象或者类能够在一 起工作.适配器将对它接口的调用翻译成对原始接口的调用,而实现这样功能的代 码通常是最简的。

上面的适配器也是每个不同的对象都要一个适配器(面版模式)

后语

希望看到这里朋友可以动动手点个赞👍哦,你们的支持就是对我最大的鼓励💪💪💪!!!