js设计模式

536 阅读24分钟

设计模式是解决某个特定场景下对某种问题的解决方案。

因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。 设计模式是一套反复使用,思想成熟,经过分类和无数实战设计经验的总结,使用设计模式是为了 让代码可复用,可扩展,可解耦,更容易被人理解且能

1、单例模式

单例模式是一种十分常用却相对比较简单的模式
在一个类中只能含有一个实例,即使多次实例化这个类,也只会返回第一次实例化后的实例对象

优点:
 1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
 2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
 3.提供了对唯一实例的受控访问。
 4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
 5.允许可变数目的实例。
 6.避免对共享资源的多重占用。
缺点:
 1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
 2. 单例模式的扩展性比较差,没有抽象层。
 3. 如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
适用场景:
 1. 需要频繁实例化然后销毁的对象。
 2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
代码实现

// es5
let single = (function(){
   // 声明一个类
   function People(name,sex,age){
       this.name = name
       this.sex = sex
       this.age = age
   }
   // 创造一个变量,准备接受实例对象
   let instance = null
   // 返回这个实例
   return function(name,sex,age){
       // 判断实例是否存在
       if(!instance){
           instance = new People(name,sex,age)
           return instance
       }
       return instance
   }
})()
// 利用single创建对象
var p = single('小红','女',18)
// 如果上面实例过一次后,就不能再实例新的对象了
var p1 = single('小明','男',108)
// es6
class SingletonApple1 {
 constructor(name,sex,age) {
   this.name = name
   this.sex = sex
   this.age = age
 }
 //静态方法
 static getInstance(name,sex,age) {
   if(!this.instance) {
     this.instance = new SingletonApple1(name,sex,age);
   }
   return this.instance;
 }
}
let appleCompany = SingletonApple1.getInstance('小红','女',18);
let copyApple = SingletonApple1.getInstance('小明','男',108)

console.log(appleCompany === copyApple); //true

典型案例:


// 实现单体模式弹窗
var createWindow = (function(){
   var div;
   return function(){
       if(!div) {
           div = document.createElement("div");
           div.innerHTML = "我是弹窗内容";
           div.style.display = 'none';
           document.body.appendChild(div);
       }
       return div;
   }
})();
document.getElementById("Id").onclick = function(){
   // 点击后先创建一个div元素
   var win = createWindow();
   win.style.display = "block";
}
理解编写通用的单体模式

2、工厂模式

工厂模式是用来创建对象的一种最常用的设计模式。
我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。

优点:
 1. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
 2. 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”
缺点:
 1. 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
 2. 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”

适用场景
  当需要创建的对象是一系列相互关联或相互依赖的产品族时,便可以使用抽象工厂模式

简单工厂

// 简单工厂
let factory = function(role){
  function superman(){
    this.name = '超级管理员'
    this.role=['修改密码','发布消息','查看主页']
  }
  function commonMan(){
    this.name = '普通游客'
    this.role=['查看主页']
  } 
  
  switch(role){
    case 'superman':
      return new superman()
    break
    case 'commonMan':
      return new commonMan()
    break
    default:
    throw new Error('参数错误')
  }
}
let superman = factory('superman');
let man = factory('commonMan');
console.log(superman)
console.log(man)

工厂方法

let factoryMehodes = function (role) {
    if(this instanceof factoryMehodes) {
        var s = new this[role]();
        return s;
    } else {
        return new factoryMehodes(role);
    }
}

factoryMehodes.prototype = {
    admin: function() {
        this.name = '平台用户';
        this.role = ['登录页', '主页']

    },
    common: function() {
        this.name = '游客';
        this.role = ['登录页']
    },
    test: function() {
        this.name = '测试';
        this.role =  ['登录页', '主页', '测试页'];
        this.test = '我还有一个测试属性哦'
    }
}

let admin = new factoryMehodes('admin');
let common = new factoryMehodes('common');
let test = new factoryMehodes('test');
console.log(admin)
console.log(common)
console.log(test)

抽象工厂模式
  抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。
  其实抽象工厂其实是实现子类继承父类的方法

 // 抽象工厂模式 new.target指的就是这个clas类
  class User {
    constructor(type) {
      if (new.target === User) {
        throw new Error('抽象类不能实例化!')
      }
      this.type = type;
    }
  }
  
  class UserOfWechat extends User {
    constructor(name) {
      super('wechat');
      this.name = name;
      this.viewPage = ['首页', '通讯录', '发现页']
    }
  }
  
  class UserOfQq extends User {
    constructor(name) {
      super('qq');
      this.name = name;
      this.viewPage = ['首页', '通讯录', '发现页']
    }
  }
  
  class UserOfWeibo extends User {
    constructor(name) {
      super('weibo');
      this.name = name;
      this.viewPage = ['首页', '通讯录', '发现页']
    }
  }
  
  function getAbstractUserFactory(type) {
    switch (type) {
      case 'wechat':
        return UserOfWechat;
        break;
      case 'qq':
        return UserOfQq;
        break;
      case 'weibo':
        return UserOfWeibo;
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }
  
  let WechatUserClass = getAbstractUserFactory('wechat');
  let QqUserClass = getAbstractUserFactory('qq');
  let WeiboUserClass = getAbstractUserFactory('weibo');
  
  let wechatUser = new WechatUserClass('微信小李');
  let qqUser = new QqUserClass('QQ小李');
  let weiboUser = new WeiboUserClass('微博小李');
  console.log(wechatUser)
  console.log(qqUser)
  console.log(weiboUser)

3、构造器模式

构造器用于创建特定类型的对象——准备好对象以备使用,同时接受构造器可以使用的参数
创建对象

var newObject = {};

  // or
  // var newObject = Object.create(null);

  // or
  // var newObject = new Object();
  // 1\. “点号”法
  // 设置属性
  newObject.someKey = "Hello World";

  // 获取属性
  var key = newObject.someKey;

  // 2\. “方括号”法

  // 设置属性
  newObject["someKey"] = "Hello World";

  // 获取属性
  var key = newObject["someKey"];

  // ECMAScript 5 仅兼容性形式
  // For more information see: http://kangax.github.com/es5-compat-table/

  // 3\. Object.defineProperty方式

  // 设置属性
  Object.defineProperty(newObject, "someKey", {
    value: "for more control of the property's behavior",
    writable: true, //当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变
    enumerable: true, //当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
    configurable: true //当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
  });

  // 如果上面的方式你感到难以阅读,可以简短的写成下面这样:

  var defineProp = function (obj, key, value) {
    var config = {
      value: value,
      writable: true,
      enumerable: true,
      configurable: true
    };
    Object.defineProperty(obj, key, config);
  };

  // 为了使用它,我们要创建一个“person”对象
  var person = Object.create(null);

  // 用属性构造对象
  defineProp(person, "car", "Delorean");
  defineProp(person, "dateOfBirth", "1981");
  defineProp(person, "hasBeard", false);

  // 4\. Object.defineProperties方式

  // 设置属性
  Object.defineProperties(newObject, {

    "someKey": {
      value: "Hello World",
      writable: true
    },

    "anotherKey": {
      value: "Foo bar",
      writable: false
    }

  });

基本构造器

//简单的构造器模式
function Car(model, year, miles) {
    this.model = model;
    this.year = year;
    this.miles = miles;
    this.toString = function () {
      return this.model + " has done " + this.miles + " miles";
    }
}
// 我们可以示例化一个Car
var civic = new Car("Honda Civic", 2009, 20000);
var mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString());
console.log(mondeo.toString());

// 缺点:1.使继承变得困难

// 2.toString()这样的函数是为每个使用Car构造器创建的新对象而分别重新定义的,这不是最理想的,因为这种函数应该在所有的car类型实例之间共享(感觉像是java中的静态方法)

带原型的构造器


function CarOne(model, year, miles) {

   this.model = model;
   this.year = year;
   this.miles = miles;

 }
 // Object.prototype ,以避免我们重新定义原型对象
 CarOne.prototype.toString = function () {
   return this.model + " has done " + this.miles + " miles";
 };

 // 使用:

 var civic = new CarOne("Honda Civic", 2009, 20000);
 var mondeo = new CarOne("Ford Mondeo", 2010, 5000);

 console.log(civic.toString());
 console.log(mondeo.toString());
 
 // 相对于简单构造器的优化 单个 toString() 实例被所有的 Car 对象所共享

4、适配器模式

顾名思义 (Adapter)是将一个类(对象)的接口(方法或属性)转化成客户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以一些工作

使用场景
  1.使用一个已经存在的对象,但其方法或属性接口不符合你的要求;
  2.你想创建一个可复用的对象,该对象可以与其它不相关的对象或不可见对象(即接口方法或属性不兼容的对象)协同工作;   3.想使用已经存在的对象,但是不能对每一个都进行原型继承以匹配它的接口。对象适配器可以适配它的父对象接口方法或属性。

最简单的适配器模式

const arr = ['Javascript', 'book', '前端编程语言', '8月1日']
function arr2objAdapter(arr) {    // 转化成我们需要的数据结构
  return {
    name: arr[0],
    type: arr[1],
    title: arr[2],
    time: arr[3]
  }
}

const adapterData = arr2objAdapter(arr)

5、外观模式

外观模式 内部是层层相关的逻辑运算,出口就是外观,用户不用关心内部逻辑
模块模式的实体包含许多被定义为私有的方法。门面则被用来提供访问这些方法的更加简单的API:

优点:
  优势是易于使用,而且本身也比较轻量级
缺点:
  观模式被开发者连续使用时会产生一定的性能问题,因为在每次调用时都要检测功能的可用性

简单的外观模式

var module = (function() {
var _private = {
    i:5,
    get : function() {
        console.log( "current value:" + this.i);
    },
    set : function( val ) {
        this.i = val;
    },
    run : function() {
        console.log( "running" );
    },
    jump: function(){
        console.log( "jumping" );
    }
};

return {

    facade : function( args ) {
        _private.set(args.val);
        _private.get();
        if ( args.run ) {
            _private.run();
        }
        if( args.jump ){
            _private.jump()
        }
    }
};
}());

// Outputs: "current value: 10" and "running"
module.facade( {run: true, val:10} );
module.facade( {jump: true, val:10} ); 

6、代理模式

定义 为一个对象提供一个代用品或者占位符,以便控制对它的访问 代理模式:生活中有很多的代理模式的场景。
例如,明星有经纪人作为代理,老板有秘书作为代理等等,当有事情的时候,会找到经纪人或秘书,再由他们转达给明星或者老板。

优点:
  代理对象可以代替本体对象被实例化,此时本体对象未真正实例化,等到合适时机再实例化。

常规代理
例子 :刘备三顾茅庐,送诸葛亮鞋子,门童即为代理
无代理直接收到礼物

let liang = {
   // 收到礼物
   reception(gift){
     console.log('亮收到礼物:' + gift)
   }
}
// 调用方法
bei.invite()
// 普通代理
// 刘备送礼物,被门童接受了
let bei1 = {
  invite(){
    doorman.reception('草鞋')
  }
}

let doorman = {
  reception(gift){
    console.log("门童接收到礼物:"+gift)
    // 转送给诸葛亮
    liang1.reception('草鞋')
  }
}

let liang1 = {
  reception(gift){
    console.log("诸葛亮在门童里接收到来自刘备的礼物:"+gift)
  }
}
// 调用
bei1.invite()

保护代理
例子:诸葛亮不接受草鞋这种礼物,所以在门童那里可以回绝

let bei2 = {
  invite(gift){
    doorman1.reception(gift)
  },
  sendBack(gift){
    console.log('刘备收到了退回的礼物:'+ gift)
  }
}
// 门童会将收到除草鞋以外的礼物给诸葛亮
let doorman1 = {
  reception(gift){
    if(gift==="草鞋"){
      bei2.sendBack(gift)
    }else{
      liang2.reception(gift)
    }
  }
}

let liang2 = {
  reception(gift){
    console.log("诸葛亮在门童里接收到来自刘备的礼物:"+gift)
  }
}
// 调用
bei2.invite('草鞋')
bei2.invite('求婚钻戒')

虚拟代理 例子:假设刘备一直送礼,诸葛亮不接受,所以他准备给钱给门童,让他买诸葛亮喜欢的礼物给他

// 虚拟代理  

let bei3 = {
  invite(gift){
    doorman2.reception(gift)
  },
  sendBack(gift){
    console.log('刘备收到了退回的礼物:'+ gift)
    // 一直被退回,所以给钱给书童,让他代买礼物
    doorman2.substitute('money')
  }
}
// 门童会将收到除草鞋以外的礼物给诸葛亮
let doorman2 = {
  reception(gift){
    if(gift==="草鞋"){
      bei3.sendBack(gift)
    }else{
      liang3.reception(gift)
    }
  },
  // 代买
  substitute(money){
    console.log(gift.gift('代买礼物;书本'))
    liang3.reception(gift.gift('代买礼物;书本'))
  }
}

let gift = {
  gift(type){
    return type
  }
}
let liang3 = {
  reception(gift){
    console.log("诸葛亮在门童里接收到来自刘备的礼物:"+gift)
  }
}
// 调用
bei3.invite('草鞋')

典型案例,图片未加载之前有图片提示

// 创建代理对象
var proxyImage = (function(){
  // 创建一个新的img标签
  var img = new Image;
  // img 加载完成事件
  img.onload = function(){
    // 调用 myImage 替换src方法
    myImage.setSrc( this.src );
  }
  return {
    // 代理设置地址
    setSrc: function( src ){
      // 预加载 loading
      myImage.setSrc( './timg.jfif' );
      // 赋值正常图片地址
      img.src = src;
    }
  }
})();

proxyImage.setSrc( 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1587127675572&di=03cbf31b15e9fbc4fcb2853cff65915e&imgtype=0&src=http%3A%2F%2Fn.sinaimg.cn%2Ftranslate%2F20170925%2FcVDm-fymfcih4510825.jpg' );

缓存代理:
 缓存代理的含义就是对第一次运行时候进行缓存,当再一次运行相同的时候,直接从缓存里面取,这样做的好处是避免重复一次运算功能,如果运算非常复杂的话,对性能很耗费,那么使用缓存对象可以提高性能;我们可以先来理解一个简单的缓存列子,就是网上常见的加法和乘法的运算。代码如下


// 计算乘法
var mult = function(){
    var a = 1;
    for(var i = 0,ilen = arguments.length; i < ilen; i+=1) {
        a = a*arguments[i];
    }
    return a;
};
// 计算加法
var plus = function(){
    var a = 0;
    for(var i = 0,ilen = arguments.length; i < ilen; i+=1) {
        a += arguments[i];
    }
    return a;
}
// 代理函数
var proxyFunc = function(fn) {
    var cache = {};  // 缓存对象
    return function(){
        var args = Array.prototype.join.call(arguments,',');
        if(args in cache) {
            return cache[args];   // 使用缓存代理
        }
        return cache[args] = fn.apply(this,arguments);
    }
};
var proxyMult = proxyFunc(mult);
console.log(proxyMult(1,2,3,4)); // 24
console.log(proxyMult(1,2,3,4)); // 缓存取 24
 
var proxyPlus = proxyFunc(plus);
console.log(proxyPlus(1,2,3,4));  // 10
console.log(proxyPlus(1,2,3,4));  // 缓存取 10
回到顶部

7、组合模式

组合模式 描述了一组对象可像单个对象一样的对待。把东西组合起来实现一个更加复杂的的功能
这允许我们能统一的处理单个对象或多个对象。这意味着无论是一个对象还是一千个对象我们都能以同样的行为来处理。

优点:
  1、高层模块调用简单。
  2、节点自由增加。

缺点:
  在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则(高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象)

简单组合模式实例:

/ 现在我们不妨来想一个智能家居的场景。家里 A、B、C 三个人,都想在到家之前一切都妥妥地:
  // A 想回到家前电视打开,饮水机开始加热。
  // B 想回到家时客厅电灯提前打开,电视打开,饮水机开始加热。
  // C 想回到家前扫地机器人开始打扫,热水器开始加热。
  // 那么现在我们可以给他们三个人每人配一个快捷指令集。我们不妨假设有一个可以定时执行指令的处理器。
  const television = {
    add(order) {
      throw Error('对不起,不能给我添加指令')
    },
    open: () => {
      console.log('电视已打开')
    }
  }
  // 饮水机
  const water = {
    add(order) {
      throw Error('对不起,不能给我添加指令')
    },
    open: () => {
      console.log('饮水机已打开')
    }
  }
  // 热水器
  const heater = {
    add(order) {
      throw Error('对不起,不能给我添加指令')
    },
    open: () => {
      console.log('热水器已打开')
    }
  }
  // 电灯
  const lamp = {
    add(order) {
      throw Error('对不起,不能给我添加指令')
    },
    open: () => {
      console.log('电灯已打开')
    }
  }
  // 扫地机器人
  const cleaner = {
    add(order) {
      throw Error('对不起,不能给我添加指令')
    },
    open: () => {
      console.log('扫地机器人已开始工作')
    }
  }
  // 快捷指令集
  class HandlerCenter {
    orderList = []
    constructor() { }
    add(...order) {
      this.orderList = order
    }
    open() {
      for (let item of this.orderList) {
        item.open()
      }
    }
  }
  // 接下来我们给A、B、C 这三个人组合他们各自所的指令
  // A
  const aHandlerCenter = new HandlerCenter()
  aHandlerCenter.add(television, water)
  // B
  const bHandlerCenter = new HandlerCenter()
  bHandlerCenter.add(lamp, television, water)
  // C
  const cHandlerCenter = new HandlerCenter()
  cHandlerCenter.add(cleaner, heater)

  // aHandlerCenter.open()
  // bHandlerCenter.open()
  // cHandlerCenter.open()

  // 上面的 A、B、C 分别执行自己的指令集。

// 只要 A、B、C 三人愿意,我们甚至还可以把三个人的指令又组合到一个指令集里,像下面这样子
const abcHandlerCenter = new HandlerCenter()
abcHandlerCenter.add(aHandlerCenter, bHandlerCenter, cHandlerCenter)
abcHandlerCenter.open()

// 我们可以一层一层叠加,即东西组合起来实现一个更加复杂的的功能,称为组合模式
// http://yunkus.com/post/5e89edecb92397c5

8、命令模式

命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

优点:
  1.它能较容易地设计一个命令队列
  2.在需要的情况下,可以较容易地将命令记入日志;
  3.允许接受请求的一方决定是否要否决请求;
  4.可以容易地实现对请求的撤销和重做;
  5.由于加进新的命令类不影响其他类,因此增加新的具体命令类很容易;
  还有最关键的优点,命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。

缺点:
 使用命令模式可能会导致某些系统有过多的具体命令类。

基本命令模式

// 添加命令
    let setCommand = function (button, func) {
      button.onclick = function () {
        func();
      }
    };
    let MenuBar = {
      refersh: function () {
        alert("刷新菜单界面");
      }
    };
    let SubMenu = {
      add: function () {
        alert("增加菜单");
      }
    };
    // 刷新菜单
    let RefreshMenuBarCommand = function (receiver) {
      return function () {
        receiver.refersh();
      };
    };
    // 增加菜单
    let AddSubMenuCommand = function (receiver) {
      return function () {
        receiver.add();
      };
    }; // 刷新菜单
    let refershMenuBarCommand = RefreshMenuBarCommand(MenuBar);
    // 增加菜单
    let addSubMenuCommand = AddSubMenuCommand(SubMenu);
    // 命令
    let b1 = document.getElementById("button1")
    let b2 = document.getElementById("button2")
    setCommand(b1, refershMenuBarCommand);
    setCommand(b2, addSubMenuCommand);

宏命令:
 宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
 这样类似把页面的所有函数方法放在一个数组里面去,然后遍历这个数组,依次执行该方法。

let command1 = {
      execute: function () {
        console.log(1);
      }
    };
    let command2 = {
      execute: function () {
        console.log(2);
      }
    };
    let command3 = {
      execute: function () {
        console.log(3);
      }
    };
    // 定义宏命令,command.add方法把子命令添加进宏命令对象,
    // 当调用宏命令对象的execute方法时,会迭代这一组命令对象,
    // 并且依次执行他们的execute方法。
    let command = function () {
      return {
        commandsList: [],
        add: function (command) {
          this.commandsList.push(command);
        },
        execute: function () {
          for (let i = 0, commands = this.commandsList.length; i < commands; i += 1) {
            this.commandsList[i].execute();
          }
        }
      }
    };
    // 初始化宏命令
    let c = command();
    c.add(command1);
    c.add(command2);
    c.add(command3);
    c.execute();  // 1,2,3

9、观察者模式

定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。

模式特征:
  1.一个目标者对象 Subject,拥有方法:添加 / 删除 / 通知 Observer
  2.多个观察者对象 Observer,拥有方法:接收 Subject 状态变更通知并处理;
  3.Subject 添加一系列 Observer, Subject 负责维护与这些 Observer 之间的联系,“你对我有兴趣,我更新就会通知你”

优点:目标者与观察者,功能耦合度降低,专注自身功能逻辑;观察者被动接收更新,时间上解耦,实时接收目标者更新状态。

缺点:观察者模式虽然实现了对象间依赖关系的低耦合,但却不能对事件通知进行细分管控,如 “筛选通知”,“指定主题事件通知” 。

//  目标类
class Subject{
  constructor(){
    this.observer = [] ; //观察者列表
    
  }
  // 添加 
  add(observer){
    this.observer.push(observer)
  }

  // 删除
  remove(observer){
    let idx = this.observer.findIndex(value => value === observer)
    idx>-1 && this.observer.splice(idx,1)
  }

  // 通知
  notify(){
    for(let observer of this.observer){
      observer.update()
    }
  }
}
// 观察者类
class Observer{
  constructor(name){
    this.name = name
  }
  update(){
    console.log("目标通知我更新了,我是" + this.name)
  }
}

// 实例化目标者
let subject = new Subject();
// 实例化两个观察者
let obs1 = new Observer("前端开发者")
let obs2 = new Observer("后端开发者")
// 向目标添加观察者
subject.add(obs1)
subject.add(obs2)
// 目标通知更新
subject.notify()

10、发布订阅模式

发布/订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似“中介者” 的角色,把订阅者和发布者联系起来。

加强版的观察者模式

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

// 控制中心
let pubSub = {
  list: {},
 
  // 订阅
  subscribe: function(key, fn) {
    if (!this.list[key]) this.list[key] = [];
 
    this.list[key].push(fn);
  },
 
  //取消订阅
  unsubscribe: function(key, fn) {
    let fnList = this.list[key];
 
    if (!fnList) return false;
 
    if (!fn) { // 不传入指定的方法,清空所用 key 下的订阅
      fnList && (fnList.length = 0);
    } else {
      fnList.forEach((item, index) => {
        item === fn && fnList.splice(index, 1);
      });
    }
  },
 
  // 发布
  publish: function(key, ...args) {
    for (let fn of this.list[key]) fn.call(this, ...args);
  }
}
 
// 订阅
pubSub.subscribe('onwork', time => {
  console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
  console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
  console.log(`吃饭了:${time}`);
})
 
pubSub.subscribe('onwork', work => {
  console.log(`上班了:${work}`);
})
 
// 发布
pubSub.publish('offwork', '18:00:00');
pubSub.publish('launch', '12:00:00');
 
// 取消订阅
pubSub.unsubscribe('onwork');

11、模块方法模式

模板方法是基于继承的设计模式,可以很好的提高系统的扩展性
javascript中的抽象父类、子类 模板方法有两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类

优点:
 1、封装不变部分,扩展可变部分。
 2、提取公共代码,便于维护。
 3、行为由父类控制,子类实现。

缺点:
 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

基本实现

// 例;来对比下泡茶和泡咖啡过程中的异同
// 泡茶 烧开水=>浸泡茶叶=>倒入杯子=>加柠檬
// 泡咖啡 烧开水=>冲泡咖啡=>倒入杯子=>加糖
const Drinks = function () { }
Drinks.prototype.firstStep = function () {
    console.log('烧开水')
}
Drinks.prototype.secondStep = function () { }
Drinks.prototype.thirdStep = function () {
    console.log('倒入杯子')
}
Drinks.prototype.fourthStep = function () { }
Drinks.prototype.init = function () { // 模板方法模式核心:在父类上定义好执行算法
    this.firstStep()
    this.secondStep()
    this.thirdStep()
    this.fourthStep()
}

const Tea = function () { }

Tea.prototype = new Drinks

Tea.prototype.secondStep = function () {
    console.log('浸泡茶叶')
}

Tea.prototype.fourthStep = function () {
    console.log('加柠檬')
}

const Coffee = function () { }

Coffee.prototype = new Drinks

Coffee.prototype.secondStep = function () {
    console.log('冲泡咖啡')
}

Coffee.prototype.fourthStep = function () {
    console.log('加糖')
}

const tea = new Tea()
tea.init()

// 烧开水
// 浸泡茶叶
// 倒入杯子
// 加柠檬

const coffee = new Coffee()
coffee.init()

// 烧开水
// 冲泡咖啡
// 倒入杯子
// 加糖

假如客人不想加佐料(糖、柠檬)怎么办,这时可以引入钩子来实现之,实现逻辑如下:

const Drinks = function () { }
  Drinks.prototype.firstStep = function () {
    console.log('烧开水')
  }
  Drinks.prototype.secondStep = function () { }
  Drinks.prototype.thirdStep = function () {
    console.log('倒入杯子')
  }
  Drinks.prototype.fourthStep = function () { }

  Drinks.prototype.ifNeedFlavour = function () { // 加上钩子
    return true
  }

  Drinks.prototype.init = function () { // 模板方法模式核心:在父类上定义好执行算法
    this.firstStep()
    this.secondStep()
    this.thirdStep()
    if (this.ifNeedFlavour()) { // 默认是 true,也就是要加调料
      this.fourthStep()
    }
  }

  const Coffee = function () { }

  Coffee.prototype = new Drinks
  Coffee.prototype.secondStep = function () {
    console.log('冲泡咖啡')
  }

  Coffee.prototype.fourthStep = function () {
    console.log('加糖')
  }
  Coffee.prototype.ifNeedFlavour = function () {
    return true // 默认添加
  }
  const coffee = new Coffee
  coffee.init()

12、策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换

策略模式的优点:
  一、策略模式可以有效避免很多if条件语句
  二、策略模式符合开放-封闭原则,使代码更容易理解和扩展
  三、策略模式中的代码可以复用
可以避免

var calculateBouns = function(salary,level) {
    if(level === 'A') {
        return salary * 4;
    }
    if(level === 'B') {
        return salary * 3;
    }
    if(level === 'C') {
        return salary * 2;
    }
};
// 调用如下:
console.log(calculateBouns(4000,'A')); // 16000
console.log(calculateBouns(2500,'B')); // 7500

基本策略模式

// 案例1 快要圣诞节,所以咧,超市中一部分商品8折出售,一部分9折出售,等到元旦还要搞大事,普通用户满100返30,VIP用户满100返50,
// 定义一个策略对象
let priceceStrategy = function(){
  // 内部算法对象 
  let strategy = {
    return30(price){
      return price - parseInt(price / 100) * 30
    },
    return50(price){
      return price - parseInt(price / 100) * 50
    },
    price80(price){
      return price  * 80 / 100
    },
    price90(price){
      return price  * 90 / 100
    }
  }
  // 策略方法调用接口
  return {
    strategyFunction(type, price) {
      return strategy[type] && strategy[type](price)
    },
    // 添加算法
    addStrategy(type, fn){
      strategy[type] = fn
    }
  }
}()

console.log(priceceStrategy.strategyFunction('return30', 100))  // 70
console.log(priceceStrategy.strategyFunction('return50', 100))  // 50
console.log(priceceStrategy.strategyFunction('price80', 100)) // 80
console.log(priceceStrategy.strategyFunction('price90', 100))// 90
// 添加一个算法
priceceStrategy.addStrategy('return70', (price) => {
  return price - parseInt(price / 100) * 70
})
console.log(priceceStrategy.strategyFunction('return70', 100)) //30

实战:表单添加验证

// 实战2 表单验证

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
<form action = "http://www.baidu.com" id="registerForm" method = "post">
  <p>
      <label>请输入用户名:</label>
      <input type="text" name="userName"/>
  </p>
  <p>
      <label>请输入密码:</label>
      <input type="text" name="password"/>
  </p>
  <p>
      <label>请输入手机号码:</label>
      <input type="text" name="phoneNumber"/>
  </p>
  <button id="registerForm">提交</button>
</form>
</body>

<script>

// 策略对象
var strategys = {
    isNotEmpty: function(value,errorMsg) {
        if(value === '') {
            return errorMsg;
        }
    },
    // 限制最小长度
    minLength: function(value,length,errorMsg) {
        if(value.length < length) {
            return errorMsg;
        }
    },
    // 手机号码格式
    mobileFormat: function(value,errorMsg) {
        if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    } 
};
var Validator = function(){
    this.cache = [];  // 保存效验规则
};
Validator.prototype.add = function(dom,rules) {
    var self = this;
    for(var i = 0, rule; rule = rules[i++]; ){
        (function(rule){
            var strategyAry = rule.strategy.split(":");
            var errorMsg = rule.errorMsg;
            self.cache.push(function(){
                var strategy = strategyAry.shift();
                strategyAry.unshift(dom.value);
                strategyAry.push(errorMsg);
                return strategys[strategy].apply(dom,strategyAry);
            });
        })(rule);
    }
};
Validator.prototype.start = function(){
    for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) {
    var msg = validatorFunc(); // 开始效验 并取得效验后的返回信息
    if(msg) {
        return msg;
    }
    }
};
// 代码调用
var registerForm = document.getElementById("registerForm");
var validateFunc = function(){
    var validator = new Validator(); // 创建一个Validator对象
    /* 添加一些效验规则 */
    validator.add(registerForm.userName,[
        {strategy: 'isNotEmpty',errorMsg:'用户名不能为空'},
        {strategy: 'minLength:6',errorMsg:'用户名长度不能小于6位'}
    ]);
    validator.add(registerForm.password,[
        {strategy: 'minLength:6',errorMsg:'密码长度不能小于6位'},
    ]);
    validator.add(registerForm.phoneNumber,[
        {strategy: 'mobileFormat',errorMsg:'手机号格式不正确'},
    ]);
    var errorMsg = validator.start(); // 获得效验结果
    return errorMsg; // 返回效验结果
};
// 点击确定提交
registerForm.onsubmit = function(){
    var errorMsg = validateFunc();
    if(errorMsg){
        alert(errorMsg);
        return false;
    }
}
</script>
</html>

13、迭代器模式(必须为有序集合,object不是有序集合)

顺序访问一个集合
使用者无需知道集合的内部结构(封装)
迭代器模式通常都是对一个数组,集合等进行访问,迭代器的设计是为了封装一个方法,可以对多种数据类型进行访问。

基本迭代器

class Iterator {
      constructor(conatiner) {
        this.list = conatiner.list
        this.index = 0
      }
      next() {
        if (this.hasNext()) {
          return this.list[this.index++]
        }
        return null
      }
      hasNext() {
        if (this.index >= this.list.length) {
          return false
        }
        return true
      }
    }

    class Container {
      constructor(list) {
        this.list = list
      }
      getIterator() {
        return new Iterator(this)
      }
    }

    // 测试代码
    let container = new Container([1, 2, 3, 4, 5])
    let iterator = container.getIterator()
    while(iterator.hasNext()) {
      console.log(iterator.next())
    }

es6典型迭代器

// es6 Iterator 
    let arr = [1, 2, 3, 4]
    let nodeList = document.getElementsByTagName('p')
    let m = new Map()
    m.set('a', 100)
    m.set('b', 200)


    function each(data) {
      // 生成遍历器
      let iterator = data[Symbol.iterator]()

      // console.log(iterator.next())  // 有数据时返回 {value: 1, done: false}
      // console.log(iterator.next())
      // console.log(iterator.next())
      // console.log(iterator.next())
      // console.log(iterator.next())  // 没有数据时返回 {value: undefined, done: true}

      let item = {done: false}
      while (!item.done) {
          item = iterator.next()
          if (!item.done) {
              console.log(item.value)
          }
      }
    }
    each(arr)

14、中介者模式

所有的相关 对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可

优点:
  1、降低了类的复杂度,将一对多转化成了一对一。   2、各个类之间的解耦。
  3、符合迪米特原则(迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话)。

缺点:
  中介者会庞大,变得复杂难以维护

图解:

未使用中介者

使用中介者

示例:
多个对象,指的不一定得是实例化的对象,也可以将其理解成互为独立的多个项。当这些项在处理时,需要知晓并通过其他项的数据来处理。 如果每个项都直接处理,程序会非常复杂,修改某个地方就得在多个项内部修改 我们将这个处理过程抽离出来,封装成中介者来处理,各项需要处理时,通知中介者即可。

const player = function (name) {
    this.name = name
    playerMiddle.add(name)
  }
  player.prototype.win = function () {
    playerMiddle.win(this.name)
  }
  player.prototype.lose = function () {
    playerMiddle.lose(this.name)
  }
  const playerMiddle = (function () { //将就用下这个demo, 这个函数充当中介者
    const players = []
    const winArr = []
    const loseArr = []
    return {
      add: function (name) {
        players.push(name)
      },
      win: function (name) {
        winArr.push(name)
        if (winArr.length + loseArr.length === players.length) {
          this.show()
        }
      },
      lose: function (name) {
        loseArr.push(name)
        if (winArr.length + loseArr.length === players.length) {
          this.show()
        }
      },
      show: function () {
        for (let winner of winArr) {
          console.log(winner + '挑戰成功;')
        }
        for (let loser of loseArr) {
          console.log(loser + '挑战失败;')
        }
      }
    }
  }())
  const a = new player('A选手')
  const b = new player('B选手')
  const c = new player('C选手')
  a.win()
  b.lose()
  c.win()
  // A 选手挑战成功;
  // B 选手挑战成功;
  // C 选手挑战失败;
  // 在这段代码中 A、B、C 之间没有直接发生关系, 而是通过另外的 playerMiddle 对象建立链接