3
作为一个前端新人,学习了设计模式以后,希望能从源头上,用浅显易懂的语言来解释它。当然不一定是正确的,只是我个人对设计模式的一点浅显理解。

创建型设计模式

创建型设计模式:故名思意,这些模式都是用来创建实例对象的。

单例模式

首先我们需要理解什么是单例。
单:指的是一个。
例:指的是创建的实例。
单例:指的是创建的总是同一个实例。也就是使用类创建的实例始终是相同的。
我们先看下面的一段代码:


class Person{
  constructor(){}
}
let p1 = new Person();
let p2 = new Person();
console.log(p1===p2)  //false

上面这段代码,定义了一个Person类,通过这个类创建了两个实例,我们可以看到最终这两个实例是不相等的。也就是说,通过同一个类得到的实例不是同一个(这本就是理所应当),但是如果我们想始终得到的是同一个实例,那么这就是单例模式。那么应该如何实现单例模式了:
想要实现单例模式,我们需要注意两点:

  1. 需要使用return。使用new的时候如果没有手动设置return,那么会默认返回this。但是,我们这里要使得每次返回的实例相同,也就是需要手动控制创建的对象,因此这里需要使用return
  2. 我们需要每次return的是同一个对象。也就是说实际上在第一次实例的时候,需要把这个实例保存起来。再下一个实例的时候,直接return这个保存的实例。因此,这里需要用到闭包了

代码实现如下:

(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变量来保存创建的实例,每次返回的都是第一次创建的实例。这样的话就实现了无论创建多少次,创建的都是同一个实例,这就是单例模式。

工厂模式

对于工厂来说,我们的印象可能是里面具有各种各样的模具,根据你想要的产品的模型,生产你需要的产品。比如说你请工厂帮你加工一个产品,你只需要告诉工厂你这个产品的结构,工厂就会有对应的模型帮你生产,你不需要去关心它具体是怎么加工的。同样工厂模式也是这样,(工厂模式也是创建型设计模式,用于创建实例对象的)你不需要自己去找对应的类来创建实例,你只需要告诉工厂类你要创建什么实例,他就会返回你需要的实例对象。
工厂模式根据抽象程度的不同,分为三种:

  1. 简单工厂模式
  2. 工厂方法模式
  3. 抽象工厂模式

简单工厂模式
定义:定义一个工厂类,通过工厂函数,根据传入的参数不同,返回不同的实例。看下面的代码:

//学生类
class Student{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  showName(){
    console.log(this.name)
  }
}

//老师类
class Teacher{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  showName(){
    console.log(this.name)
  }
}

//警察类
class Policeman{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  showName(){
    console.log(this.name)
  }
}
根据类创建对象
const s1 = new Student('王小一',24);
const t1 = new Teacher('李一老师',39);
const p1= new Policeman('张一警官',40);

我们可以看到,上面代码中定义了三个类,学生类,老师类和警察类。而且它们具有相同的属性和方法。当我们需要创建学生实例时,我们调用学生类。当我们需要创建老师实例时,我们调用老师类,当我们需要创建警察实例,我们调用警察类。假设我们有更多的人物类,它们具有相同的功能,那么当我们需要创建实例的时候,我们同样需要调用相对应的类。事实上,这些类实现的都是相同的功能,那么我们可不可以把所有的创建这些人物实例都通过一个类来实现了。我们尝试将代码修改为如下:

//学生类
class Student{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  showName(){
    console.log(this.name)
  }
}

//老师类
class Teacher{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  showName(){
    console.log(this.name)
  }
}

//警察类
class Policeman{
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  showName(){
    console.log(this.name)
  }
}

//工厂类
class Factory{
  let obj = null;
  //工厂函数
  constructor(role,name,age){
    switch(role){
      case 'student':
         obj = new Student(name,age);
         break;
     case 'teacher':
         obj = new Teacher(name,age);
         break;
     case 'policeman':
         obj = new Policeman(name,age);
         break;
    }
  }
 return obj;
}

const s2 = new Factory('student','王小二',25);
const t2 = new Factory('teacher','李二老师',39);
const p2 = new Factory('policeman','张二警官',40);

从上面的代码中,我们可以看到我们同样定义了学生类,老师类,警察类这三个类,但是我们创建实例时通过Factory这个类,不再通过相对应的人物类了。这个Factory类就是工厂类,我们观察工厂类的实现,发现里面是一个工厂函数(这里直接使用了constructor,也可以自己定义工厂函数),通过传递给工厂函数的参数不同,返回不同的实例。这就是简单工厂模式。

简单工厂模式总结
实现:从上面的代码中我们可以知道,所谓简单工厂模式就是一个工厂类和一个工厂函数,通过传入参数的不同,返回不同的实例。
特点:1. 需要创建的类较少,因为需要根据传入的参数来判断返回的实例,如果类太多,那么就会导致逻辑复杂。2. 不需要关注实例的创建过程,只需要传入相对应的值即可。
适用场景:举一个生活中实际的使用场合,假如我们上体育课需要去拿篮球,足球和排球,我们可以自己去一个一个找对应的球(类似于上面通过自己来创建对象),也可以通过管理员,告诉管理员需要什么样的球,至于管理员是怎么找到这个相对应的球,就与我们不相关了。这个管理员就是工厂类。
缺点:从简单工厂模式的特点中我们可以知道,简单工厂模式适合于创建的类较少,一旦需要的类较多,逻辑就会复杂。而且一旦需要添加新的类,就得重新修改工厂类,这样显得非常不方便。

工厂方法模式
工厂方法模式是对简单工厂的进一步优化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。说的好像挺复杂,其实在我看来他就是解决简单工厂模式存在的不方便添加新的类,因为添加新的类以后需要修改工厂函数。而工厂方法模式就是解决这个问题,看下面的代码:

let Factory = (function(){
  let s = {
    Student(name,age){
      this.name = name;
      this.age = age;
      return this;
    },
    Teacher(name,age){
      this.name = name;
      this.age = age;
      return this;
    },
    Policeman(name,age){
      this.name = name;
      this.age = age;
      return this;
    },
    //在这里添加新的类
    Doctor(name,age){
      this.name = name;
      this.age = age;
      return this;
    }
  }

  return class {
    //工厂函数根据传进来的来的参数不同而不同。
    constructor(type,name,age){
      if(s.hasOwnProperty(type)){
        return s[type].call(this,name,age)
      }else{
        throw new Error(`不存在${type}类`)
      }

    }
  }


})()

let s3 = new Factory('Student','王小三',25);
let t3 = new Factory('Teacher','李三老师',25);
let p3 = new Factory('Policeman','张三警官',28);
let d3 = new Factory('Doctor','杨医生',33);

从上面的代码中,我们可以看到,相比于简单工厂函数,工厂方法模式的工厂函数不是固定的,而是根据type不同而不同。当我们需要添加新的类时,只需要在s对象中添加实例即可,不需要修改工厂函数。这样的话就不会因为需要添加新的类,而修改过多的代码逻辑。这就是工厂方法模式。其实就是对简单工厂模式的优化而已。

建造者模式(builder pattern)

提到建造者,我们可能第一印象就是城市中建高楼大厦,建房子一般设计到业主,项目负责人,建筑队工人。业主告诉项目负责人,需要建造什么样的房子,项目负责人告诉工人应该怎么修建,不同工人完成不同工作。大家各司其职,最终得到一个建成的房子。虽然在建房子过程中,各个部分都需要打交道,但是更多的还是各司其职,每个人完成每个人的工作。其实就是把一个复杂的建房子工程,拆分成了若干部分由不同人来完成。在编程中也是如此,如果我们需要创建一个复杂的对象,可以把这个对象进行构建,使得不同部分,完成不同功能。
建造者模式定义:将一个复杂的对象分解成多个简单的对象来进行构建,将复杂的构建层与表示层分离,使得相同的构建过程可以创建不同的表示的模式便是建造者模式。看定义通常是无法直接理解这种设计模式的,还是直接看代码:
假设我们需要创建一辆车,车的组件包括车名,车牌号,车的价钱,车的引擎等。我们先看不使用建造者模式应该如何创建:

class Car{
  constructor(){
    this.name = '';
    this.number = '';
    this.price = '';
    this.engine = '';
  }
//设置名字
  setName(){
    this.name = '宝马';
  }
//设置车牌号
  setNumber(){
    this.number = '888888'
  }
//设置价钱
  setPrice(){
    this.price = '100万'
  }
//设置引擎
  setEngine(){
    this.engine = '最好的引擎'
  }
//车的创建
  getCar(){
    this.setName();
    this.setNumber();
    this.setPrice();
    this.setEngine();
  }
}

//创建一个车:
let car = new Car();
car.getCar();
console.log(car)

从上面的代码中,我们可以看到创建一辆车需要的元素包括:name,number,price,engine。每一种元素又需要setxx来单独实现,最终车的创建还需要通过getCar来完成。也就是说在创建车的过程中需要的元素较多,创建过程相互影响,相互耦合。这只是简单的4个元素,而且耦合性也不是太高,但是假设元素他特别多,代码的耦合性也特别多,如果出现添加新的要素,那么实现起来要修改的代码就太多了。因此,我们需要对代码进行解耦,这就是建造者模式。
上面我们提到了建造一个房子,需要业主,项目负责人,建筑工人。其实建造者模式也包括这三个类:产品类(客户提出产品需要),指挥者类,建造者类。
建造者模式的使用流程如下:

  1. 客户提出产品需求:比如上面产品就是一辆小汽车,产品要素包括name,number,price,engine
  2. 指挥者根据产品需求,安排建造者完成需求的各个部分
  3. 建造者完成相应的部分

使用建造者模式修改上面的代码如下:

//产品类:产品要素
class Car{
  constructor(){
    this.name = '';
    this.number = '';
    this.price = '';
    this.engine = '';
  }
}


//建造者类:各种工人完成相应的部分
class CarBuilder{
  setName(){
    this.name = '宝马';
  }
  setNumber(){
    this.number = '888888';
  }
  setPrice(price){
    this.price = '100万';
  }
  setEngine(engine){
    this.engine = '最好的引擎';
  }
  getCar(){
    var car = new Car();
    car.name = this.name;
    car.number = this.number;
    car.price = this.price;
    car.engine = this.engine;
    return car;
  }
}

//指挥官类:指挥工人完成各部分工作
class Director{
  action(builder){
    builder.setName();
    builder.setNumber();
    builder.setPrice();
    builder.setEngine();
  }
}

//使用方法:

let builder = new CarBuilder();
let director = new Director();
director.action(builder);
let car = builder.getCar();
console.log(car)

从上面的代码中,我们可以看出,定义了产品类,主要负责定义产品的需求;建造者类,主要负责完成需求的各个部分;指挥者类,主要负责指挥工人完成各部分工作。实际上就是把一辆车的复杂的创建过程抽离成三个简单的类来完成,大家各司其职,减少了代码的耦合。当以后需要添加新的需求时,只需要在各个部分单独定义即可,比如现在造汽车还需要安装玻璃,那么只需要在每个类里面定义玻璃相关的要素,建造者,指挥者即可。而不需要考虑代码的各部分耦合。这就是建造者模式。

原型模式

原型模式:通俗点讲就是创建一个共享的原型,并通过拷贝这些原型创建新的对象。在我看来,其实原型模式就是指定新创建对象的模型,更通俗一点来说就是我想要新创建的对象的原型是我指定的对象。最简单的原型模式的实现就是通过Object.create()。Object.create(),会使用现有的对象来提供新创建的对象的__proto__。

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()将person对象作为anotherPerson对象的原型,创建了anotherPerson。因此anotherPerson可以直接获得person的属性name,age等。

另外,如果我们想要自己实现原型模式,而不是使用封装好的Object.create()函数,那么可以使用原型继承来实现:

function F(){

}

F.prototype.g = function(){}


//G类继承F类

function G(){
  F.call(this);
}

//原型继承
function Fn(){}
Fn.prototype = F.prototype;
G.prototype = new Fn();



G.prototype.constructor = G;

上面的代码,学过js的应该都能看懂,没什么好解释的。
原型模式总结:
原型模式就是创建一个指定原型的对象。如果我们需要重复创建某个对象,那么就可以使用原型模式来实现。

结构性设计模式

上面的创建型设计模式,专注于创建对象。而结构性设计模式则专注于结构设计,通俗点来说就是对象与对象之间的结构设计,也就是如何将类或者对象进行更好的组织,以方便使用。

外观设计模式

外观设计模式定义:为一组复杂的子接口提供一个更高级的统一接口,以便更方便的去实现子接口的功能。看定义总是感觉很复杂,我们根据一些场景来具体分析:

HTML代码
 <div id="btn1">按钮1</div>
 <div id="btn2">按钮2</div>
js代码
    let oBtn1= document.getElementById('btn1');
    let oBtn2= document.getElementById('btn2');


    //按钮1
    function btn1Fn(){
      console.log('这里是按钮1的内容')
    }
    //DOM事件的兼容性处理
    if( document.addEventListener ){
      oBtn1.addEventListener("click" , btn1Fn, false);
    }else if(document.attachEvent){
      oBtn1.attachEvent("onclick" , btn1Fn);
    }else{
      oBtn1.onclick = btn1Fn;
    }


    //按钮2
    function btn2Fn(){
      console.log('这里是按钮2的内容')
    }
    //DOM事件的兼容性处理
    if( document.addEventListener ){
      oBtn1.addEventListener("click" , btn2Fn, false);
    }else if(document.attachEvent){
      oBtn1.attachEvent("onclick" , btn2Fn);
    }else{
      oBtn1.onclick = btn2Fn;
    }

上面代码中由两个按钮,每个按钮有对应的点击事件。但是我们知道如果直接使用onclickDOM0级点击事件,那么就可能出现后续的事件覆盖前面的。因此建议使用DOM2级点击事件,但是addEventListener存在兼容性问题,在IE中存在不兼容。因此我们需要对每次事件做兼容性处理。从上面的代码中,我们可以看出,我们对按钮1和按钮2都做了兼容性处理。事实上,这些兼容性处理都是相同的,如果每一次都去写复杂的重复的兼容性代码,是没有意义的。因此,我们通常会将兼容性处理封装起来,作为一个统一的接口,在需要的时候,直接调用这个接口,而不是再去重复写这个复杂的兼容代码,这种将复杂的代码封装起来,其实就是外观设计模式.我们看使用外观设计模式实现的代码:

let oBtn1= document.getElementById('btn1');
let oBtn2= document.getElementById('btn2');

//按钮1
function btn1Fn(){
  console.log('这里是按钮1的内容')
}
//按钮2
function btn2Fn(){
  console.log('这里是按钮2的内容')
}

function bindEvent(element,type,fn) {
  if(document.addEventListener){
    element.addEventListener(type,fn,false)
  }else if(document.attachEvent){
    element.attachEvent('on'+type,fn)
  }else{
    element["on"+type] = fn;
  }
}
bindEvent(oBtn1,'click',btn1Fn)
bindEvent(oBtn2,'click',btn2Fn)

从上面的代码中,我们可以看到使用了bindEvent来封装兼容性处理,然后oBtn1和oBtn2触发点击事件都是直接调用这个封装函数。其实这就是外观模式的一次典型应用。所有的API的兼容性封装其实都是外观模式的应用。外观模式其实就是把一些复杂的操作隐藏起来,然后我们从更高级别直接调用。我们再看我们在开发中经常使用到的一个例子:

var  person = {
  init:function(){
  //  这里是初始化代码
  },
  getAge:function(){
  //  这里是获取元素的方法

  },
  getName:function(){
  //  这里是获取样式的代码
  },
  getJob:function(){
  //这里是获取个人工作的代码
  }
}
var name = person.getName();

上面的代码是一个对象person,里面封装了与个人信息相关的age,name,job等方法,然后在需要获取这些信息时,我们使用person.xx来调用。其实这就是外观模式的另外一种应用。在开发中,我们经常会使用命名空间,将一些相关的信息都封装到这个对象中,这种将各类操作饥饿和在一起,对外提供统一的接口就是外观模式的典型应用。
外观设计模式总结:外观设计模式时结构性设计模式,我们之前说过结构型设计模式是用于组织代码结构的。而外观设计模式就是把所有的子类饥饿和在一起,对外提供统一的接口这样的一种组织形式。典型的使用就是各种兼容性API的封装和命名空间的使用。

适配器设计模式

提到适配器,我们想到的可能是电源适配器。一些电子产品的插座是三孔的,但是如果没有三孔插座,那么我们就需要使用转换器,使用两孔插座通过转换器来给三孔电子产品充电,这个转换器就是适配器。在生活中,我们经常会碰到这种由于接口不同需要通过转换来实现的情况,在实际的代码开发中,我们经常也会遇到接口不同,需要进行转换的过程。这个转换的过程就是适配。这种设计模式就是适配器设计模式。
适配器模式定义:将一个类的接口转换成另外一个接口,以满足用户需求,解决接口不一样而产生的问题。看具体的代码:

function getInfo(){
  //假设result为请求后台得到的数据,数组形式

  var result = ['henry',24,'teacher'];
  console.log('名字是'+result[0]+'年龄是'+result[1]+'职业是'+result[2])
  
}

上面的代码中,定义了一个用于获取个人信息的函数,个人信息的获取为通过后台接口返回到result中。现在返回的数据为数组形式。我们后面所有的操作都是按照数组来进行的,比如获取姓名result[0]。获取年龄为result[1]。但是,假如后端接口发生变化了,不再返回数组形式了(这在实际的开发中可能非常常见)。而实返回一个对象了。那么我们后面所有的使用数组的操作就都是错误的了,这样的话所有涉及到数据的都需要进行修改,如果后续代码非常多,那么修改起来就非常麻烦了。这时候,我们可能考虑变通一下,将返回的对象,转换成我们需要的数组即可。代码如下:

function fn2(){
  //假设result2为请求后台得到的数据,对象形式

  var result2 = {
    name:'henry',
    age:24,
    job:'teacher'
  }
  //将对象转化成数组  
  function objToArray(obj){
    var arr = [];
    for(var key in obj){
      arr.push(obj[key])
    }
    return arr;
  }
  var result =  objToArray(result2)


  console.log('名字是'+result[0]+'年龄是'+result[1]+'职业是'+result[2])

}

上面的代码中,result2为后台请求的接口,数据类型是对象。但是我们之前都是按照数组处理的,因此我们需要将对象转换成数组,函数objToArray就是用来转换的函数。这样的话,我们就不需要去修改后面的代码,这个转换的过程就是适配过程。objToArray就是适配函数。这就是适配器设计模式。
适配器设计模式总结:适配器设计模式其实就是把一些不适合我们当前使用的接口,通过适配以后,转换成能够被我们使用的接口。最常见的就是接口的转换

代理模式

提到代理,我们可能想到的是代理人。在生活中代理人最多的可能就是明星了,每个明星都有自己的代理人,我们找明星合作通常都是先接触代理人,然后才接触明星。也就是说这个代理人是在我们和明星之间进行了一次拦截,筛选出符合见明星的人。其实代理模式在开发中也是这样,只不过这里代理的是对象,而不是明星,通过为对象提供一个代理,用来控制对这个对象的访问。
代理模式定义:为对象提供一个代理,用来控制对这个对象的访问。
下面以具体代码举例:比如公司的个人信息的访问:

let person = {
  id = '1',
  name = '刘亦菲',
  age:30
}
console.log(person.name) 

上面定义了一个包含个人信息的对象person,如果没有进行代理,那么可以直接通过person.xx进行访问。但是,事实上我们不希望个人信息被查看,只有本人能够进行查看和修改。那么这时候我们可以对对象的访问添加代理。具体代码如下:

let info = (function(){
  let person = {
    id:1,
    name:'刘亦菲',
    age:24,
    job:'teacher'
  }

  return function({id}){
    //代理对象
    let handle = {
      get:function(target,key){
        if(key === 'age'){
          if(id === 1){
            return Reflect.get(target,key)
          }else{
            throw new Error('您没有权限查看该信息')
          }
        }
      },
      set:function(target,key,value){
        if(id === 1){
          return Reflect.set(target,key,value)
        }else{
          throw new Error('您没有权限修改个人信息')
        }
      }
    }
    return new Proxy(person,handle)
  }
})()

let star = info({id:1})
console.log(star.age)
star.age = 30;
console.log(star.age)

通过使用new Proxy(target,handle)来进行代理。其中target为被代理对象,handle为代理对象或者说拦截对象。在handle中我们通过定义get和set函数来进行拦截,只有id为1的人才能查看自己的个人信息。这就是代理模式。
代理模式的应用场景:
上面的例子只是简单的代理对象的访问,其实代理更多的时候是用于控制开销很大的对象的访问。比如,一个创建实例开销很大的访问,它会把创建实例放到方法被调用的时候(也就是真正需要被用到的时候),因为如果整个程序运行期间都没有用到这个对象,那么就不需要创建它,这样可以大大节省资源。比如图片的延迟加载,我们并不需要一开始就加载所有的图片,而是使用一张loading图片来代替它们,只有在真正需要展示这张图片的时候,再替代掉src即可。
代理模式的总结:
归功接底代理模式就是控制对对象的访问,无论是拦截访问,还是延迟访问都是对对象访问的控制罢了。如果你要访问一个对象,但是你不想马上访问,或者不想直接访问那么这都是代理模式。

装饰者模式

装饰是生活中很常见的行为,我们给房子搞装修,自己装扮自己的卧室等这些都能够算作装饰,那么我们为什么要搞装饰?还不是为了让房子变得更加美观,漂亮,更有特色。同样,在开发中装饰者模式也是为了给对象增加新的特性,或者说增加新的功能。
装饰者模式定义:在不创建新对象的情况下,给对象添加新的特性。就类似于在不破坏房子的情况下,给房子进行装修,我们不可能把买来的房子拆了再重新建一个再装修,同样装饰者模式是在不创建新对象的情况下,给对象增加新的特性。下面看具体代码:

 class Car{
    constructor(name,price){
      this.name = name,
      this.price = price
    }
    getCar(){
      console.log('买了这辆车')
    }
  }

  let xiaoming = new Car('宝马','100万');
  xiaoming.getCar();
  let xiaohong = new Car('丰田','50万');
  xiaohong.getCar();
  let xiaogang = new Car('大众','30万');
  xiaogang.getCar();

如上面代码所示:定义了汽车类,小明花了100万买了宝马车,小红花了50万买了丰田车,小刚花了30万买了大众车。但是这时候经销商突然进行促销了,说买宝马的可以再送两个轮胎,买丰田的可以再送购物卡,买大众的可以再送加油卡。那么这时候对于小明,小刚,小红它们来说应该怎么办了?他们应该也有这些送的东西(注意每种车送的东西不一样),我们不可能再将这些特性再添加到Car类上面去,然后再创建小明等实例去买车。也就是说我们需要再不创建新的实例的情况下,给对象添加特性,这恰好是装饰着模式的定义,下面看具体的代码实现:

  class Car{
    constructor(name,price){
      this.name = name,
        this.price = price
    }
    getCar(){
      console.log('买了这辆车')
    }
  }

  let xiaoming = new Car('宝马','100万');
  xiaoming.getCar();
  let xiaohong = new Car('丰田','50万');
  xiaohong.getCar();
  let xiaogang = new Car('大众','30万');
  xiaogang.getCar();

  
  //装饰过程
  function decoratorBaoma(){
    this.wheel = '宝马车送轮胎'
  }
  decoratorBaoma.call(xiaoming)

  function decoratorFengtian(){
    this.shoppingcard = '丰田车送购物卡'
  }
  decoratorFengtian.call(xiaohong)

  function decoratordazhong(){
    this.oil = '大众车送加油卡'
  }
  decoratordazhong.call(xiaogang)
  console.log(xiaoming)
  console.log(xiaohong)

如面代码所示:我们在没有创建新对象的情况下,定义了三个装饰函数,通过给宝马车用户添加wheel属性送轮胎,给丰田车用户添加shoppingcard属性送购物卡,给大众车用户添加oil属性送加油卡。然后执行这几个函数,修改相对应的this指向即可。这样的话就实现了我们想要的功能。这就是装饰者模式。
装饰者模式总结装饰者模式就是在不创建新对象的情况下,给对象添加新的特性。

桥接模式

桥梁的作用主要就是连接岸的两边。在开发中,桥接模式的作用也是用于连接,只不过它连接的是抽象部分和实现部分。这么说可能很抽象,我们用具体的例子来展示。桥接模式在javascript中应用最广泛的就是事件监听。

 <div id="box1">box1</div>

 var oBox1 = document.getElementById('box1');
 bindEvent(oBox1,'click',getBeerById)

    function getBeerById(){
        var id = this.id;
        asyncRequest('GET','beer.uri?id='+id,function(resp) {
          console.log(resp.responseText);
        })
    }

如上面代码所示,我们定义了一个div,给id为box1的div绑定了点击事件,事件函数为getBeerById。其中bindEvend点击事件为抽象类,getBeerById为具体实现类。我们可以发现这个就实现类,也就是getBeerById函数,只能作为事件触发函数,因为它里面的this.id依赖于点击事件即抽象类。我们没办法对getBeerById进行单元测试。现在假设我们又新增了一个div,它根据类名传递参数,代码如下所示:

<div class="box2">box1</div>

 var oBox2 = document.getElementsByClassName('box2')[0];
 bindEvent(oBox2,'click',getBeerByClassName)

    function getBeerByClassName(){
      var className = this.className;
      asyncRequest('GET','beer.uri?id='+className,function(resp) {
        console.log(resp.responseText);
      })
   }

上面这个事件处理函数getBeerByClassName,不再是根据id请求数据了,而是根据className请求数据。他们之间仅仅是传递参数的区别,根据代码复用和抽象的原则,我们可能会将代码公告部分抽离出来:

   function getBeerId(id){
     asyncRequest('GET','beer.uri?id='+id,function(resp) {
        console.log(resp.responseText);
      })
   }

将代码公共部分抽离出来后,虽然提高了代码的服用。但是带来了新的问题,由于之前的具体实现是作为事件处理函数,它依赖于抽象类即点击事件。抽象后的代码没办法作为事件处理函数了(因为之前通过this获取id和className,现在是将其作为参数进行封装了),因此为了保证原来的功能,我们需要对代码及性能修改,使用新的事件处理函数。

    //抽象类
    bindEvent(oBox1,'click',getBeerByIdBridge)

    //桥接函数
    function getBeerByIdBridge(e){
      getBeerById(this.id)
    }

    
    //具体类
    function getBeerById(id){
      asyncRequest('GET','beer.uri?id='+id,function(resp) {
        console.log(resp.responseText)
      })
    }

通过上面的代码,我们可以知道,定义了一个函数getBeerByIdBridge来作为事件处理函数。但是这个函数并没有实现具体的功能。具体功能的实现在getBeerById中,也就是说这个函数其实只是点击事件和具体功能之间连接的桥梁。这就是桥接模式,这个函数就是桥接函数。通过上面的代码,我们可以知道桥接模式就是抽象类和具体类之间抽离开来,使得他们能够各自独自变化,而不是互相依赖。
桥接模式的另一个应用:用于连接多个类。
桥接模式不仅能够用来连接抽象和具体实现,而且还能够用于连接多个类,这些多各类实现各部分功能,通过桥接模式实现完整的功能。看具体的代码如下:

//A类
 class A {
    constructor(name,age){
      this.name = name;
      this.age = age;
    }
    showName(){
      console.log(this.name)
    }
  }
  //B类
  class B {
    constructor(job,sex){
      this.job = job;
      this.sex = sex;
    }
    showJob(){
      console.log(this.job)
    }
  }


  //桥接类
  class Bridge{
    constructor(){
      this.w = new A('刘亦菲',30);
      this.h = new B('actor','女')
    }
  }

  let bridge = new Bridge();
  console.log(bridge)
  bridge.w.showName()

如上面代码所示,通过桥接类将类A和类B连接起来了,A类用于记录姓名和年龄,B类用于记录职业和性别,桥接类用于这些功能的所有的实现,相当于记录一个人的完整信息。这样的话我们进行开发时,可以分别对A类和B类进行单独开发,各部分实现各自的功能,最后再通过桥接类实现完整功能。

桥接模式的总结:桥接模式用于将抽象与其是实现隔离开来,以便二者独立变化。这种模式对于事件驱动的编程非常方便。另外桥接模式还可以用于将多个类连接起来,用于实现一个完整功能。

组合模式

组合:是指把一些零散的东西汇聚成一个整体,或者说把部分汇聚成整体。在开发中,组合模式同样如此。
组合模式的定义:组合模式又称部分-整体模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。从组合模式的定义我们可以知道,组合模式有两个特点:
1.组合模式是部分与整体的层次关系,形成树形结构
组合模式的层次关系如下如图所示:

从图中我们可以看到组合模式的整个结构是:一些叶子对象(也就是部分)组合成一级组合对象,这些组合对象又组合成一个组合对象,最终形成这种树形结构。我们以一个生活中实际的例子来举例:
图片描述

比如我们点餐,首先是整个菜单,然后菜单分为三类:主菜,饮料和甜品。每一类下面又进行划分,比如主菜包括土豆丝,西红柿炒鸡蛋,红烧牛肉等。这样形成了一个属性菜单。
另外,提到树形结构,我们学习的DOM树就是最常见的属性结构。
2.组合模式使得用户对单个对象和组合对象具有一致性。这句话怎么理解了,其实就是组合独享和单个对象都具有一些相同的API(可以这么粗暴的理解),比如都定义成同名函数。
比如,我们以jQuery操作DOM为例。

  <div class="wrap">
     <span class="box"></span>
  </div>

  <script>
    $('.wrap').css('color','red')
    $('.wrap .box').css('color','green')
  </script>

如上面代码所示:我们知道DOM结构是树形结构,同时使用jQuery时,我们既可以对div使用css方法,又可以对div的子元素使用css方法,也就是说树型结构的组合和整体对象都能够使用相同的css方法。同样我们再以刚才的订餐举例:
图片描述
从上面的图中我们可以看出,所有的对象无论是叶子对象还是组合对象都具有相同的add方法,也就是说这个结构的组合对象的API使用具有一致性。

组合模式总结:组合模式牢记两个特点:

  1. 组合对象之间能够形成树形结构
  2. 组合对象之间的API使用具有一致性。所谓一致性就是类似于同名函数。

海因斯坦
41 声望5 粉丝