JavaScript-设计模式

Ymc 2019-08-02 22:31:07 15 Page View 28.6'm Read Time 设计模式持续更新
任何一门语言设计模式都是十分重要的一个环节,就像烹饪,没有调料你同样可以烧出一份菜,但是当搭配适当的佐料,这份菜马上就会不同,coding中的设计模式同样如此,下面让我们来系统的了解设计模式吧,提高我们代码的lever,优雅高效的书写出赏心悦目的代码吧

“oo模式是我们的目标,而设计模式是我们的做法”这是 Head First设计模式模式中我记忆深刻的一句话,其实在我们日常开发中也经常用到一些设计模式,下面燃我们系统的来认识下JavaScript中的设计模式吧~

今天我们就来介绍一下几种设计模式

  • 单体(单例)模式
  • 工厂模式
  • 策略模式
  • 迭代器模式
  • 装饰者模式
  • 观察者模式(订阅/发布模式)
  • 代理模式
  • 中介者模式
  • 外观模式

JavaScript设计模式

单体模式

单体模式(Singleton Pattern)的思想在于保证一个特定类仅有一个实例,即不管使用这个类创建多少个新对象,都会得到与第一次创建的对象完全相同。

单体模式有以下优点:

  • 用来划分命名空间,减少全局变量数量。

  • 使代码组织的更一致,提高代码阅读性和维护性。

  • 只能被实例化一次。

  • 惰性单体模式,有利于优化性能(添加一个注册方法的,在使用内部方法的时候再去注册)

实战演练:

/** 保证一个特定类只能有一个实例,即不管适用类创建多少个实例,对象只有一个 */
function Singleton(name) {
  if (typeof Singleton.sign === 'object') {
    return Singleton.sign;
  }
  this.singletonName = name;
  Singleton.sign = this;
}
Singleton.prototype.getName = function() {
  return this.name;
};
// 同上面的区别 sign 不是公开的
class Singleton {
  constructor(name) {
    let sign;
    this.singletonName = name;
    sign = this;
    Singleton = function () {
      return sign;
    };
  }
}

let sing1 = new Singleton('A');
let sing2 = new Singleton('B');
sing1.spection = '论35度和37度的区别';
console.log(sing1 === sing2);
console.log('this下和实例对象下的区别');
console.log(sing1);
console.log(sing2);
console.log(Singleton);

总结:单体模式适用于类似开关的一种存在,无论你多少个对象,只会有一个对象的存在...在全局弹窗等场景比较适合使用

工厂模式

通过工厂方法(或类)创建的对象,都继承父对象

工厂模式主要实现,以下功能

  • 可重复执行,来创建相似对象。
  • 当编译时位置具体类型(类)时,为调用者提供一种创建对象的接口。

优缺点

优点

  • 一个调用者想创建一个对象,只要知道其名称就可以了。

  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

  • 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点

  • 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

实战演练
工厂模式又更具不同设计想法分为简单工厂模式、工厂方法模式、抽象工厂模式

下面我们以一个管理端不同用户角色、不同用户类型设计

简单工厂模式:

** 简单工厂模式(静态工厂方法) */
User类
class User {
  //构造器
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  //静态方法
  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
        break;
      case 'user':
        return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user');
    }
  }
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');
console.log('简单工厂模式',superAdmin, admin, normalUser);

工厂方法模式

/** 工厂方法模式 */
class User {
  constructor(name = '', viewPage = []) {
    if (new.target === User) {
      throw new Error('抽象类不能实例化!');
    }
    this.name = name;
    this.viewPage = viewPage;
  }
}

class UserFactory extends User {
  constructor(name, viewPage) {
    super(name, viewPage);
  }
  create(role) {
    switch (role) {
      case 'superAdmin':
        return new UserFactory('超级管理员', ['首页', '通讯录', '发现页', '应用数据', '权限管理']);
        break;
      case 'admin':
        return new UserFactory('普通用户', ['首页', '通讯录', '发现页']);
        break;
      case 'user':
        return new UserFactory('普通用户', ['首页', '通讯录', '发现页']);
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user');
    }
  }
}

let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');
console.log('工厂模式方法', superAdmin, admin, user);

抽象工厂模式

/** 抽象工厂模式 */
// 抽象工厂方法
function AbstractFactory(subType, superType) {
  if (typeof AbstractFactory[superType] === 'function') {
    // function F() {}
    // F.prototype = new AbstractFactory[superType]();
    // subType.constructor = subType;
    // subType.prototype = new F();

    subType.constructor = subType;
    subType.prototype = new AbstractFactory[superType]();
  } else {
    return console.log(`未创建${superType}抽象类`);
  }
}

// 定义抽象类的方法
// 超级管理员
AbstractFactory.SuperAdmin = function() {
  this.type = 'superAdmin';
};
AbstractFactory.SuperAdmin.prototype = {
  getAccountInfo: function() {
    return console.log('这是一个默认方法', 'superAdmin');
  }
};

// 管理员
AbstractFactory.Admin = function() {
  this.type = 'admin';
};
AbstractFactory.Admin.prototype = {
  getAccountInfo: function() {
    return console.log('这是一个默认方法', 'admin');
  }
};

// 用户
AbstractFactory.User = function() {
  this.type = 'user';
};
AbstractFactory.User.prototype = {
  getAccountInfo: function() {
    return console.log('这是一个默认方法', 'user');
  }
};

// 具体实现
function QQUser(name) {
  this.userName = name;
}

AbstractFactory(QQUser, 'User');

// QQUser.prototype.getAccountInfo = function() {
//   console.log(123);
//   return this.userName;
// };

// 实例化产品子蔟
let qqUser_songkejun = new QQUser('宋克军');
console.log(qqUser_songkejun);

console.log(qqUser_songkejun.type);

console.log(qqUser_songkejun.userName);

console.log(qqUser_songkejun.getAccountInfo());

前两者都比较好理解,最后一个抽象工厂模式虽然一般简单的业务遇到的不多,但是,其中的的抽象抽象类方法在我们业务比较复杂拥有多级产品蔟的结果就会发现不一样的天地

策略模式

策略模式(Strategy Pattern)封装一系列算法,使用相同的接口可选择不同的算法;目的是将一系列的算法定义和使用分离开来,

优缺点以及应用场景

  • 有效的避免了多重条件语句
  • 支持开闭原则,将算法单独封装,便于理解和拓展、提高一定的复用性能

缺点不足

  • 策略类会增加,并每个策略类都需要暴露

应用场景

  • 使用策略模式设计一个多重校验规则表单校验

实战演练

/* 传统原型模式下的策略模式 - 计算不同事物的能量(千焦)*/
class Power {
  constructor() {
    this.powerBase = null; // 事物的基础能量
    this.powerType = null; // 不同事物的差异能量
  }
  setPowerBase(power) {
    this.powerBase = power;
  }
  setPowerType(powerType) {
    this.powerType = powerType;
  }
  getPower() {
    return this.powerType.calculate(this.powerBase);
  }
}

// 策略类
class PowerTypeA {
  constructor() {}
  calculate(power) {
    return power * 1.5;
  }
}
class PowerTypeB {
  constructor() {}
  calculate(power) {
    return power * 2.5;
  }
}

let powers = new Power();
powers.setPowerBase(100);
powers.setPowerType(new PowerTypeA());
console.log(powers.getPower());

个人感觉策略模式的思路很想外观模式将不同方法抽离出来,暴露外部简单的调用接口api...

迭代器模式

迭代器模式(Iterator Pattern)提供一种方法,顺利访问一个聚合对象中的每一个元素,并且不暴露该对象内部

具有以下特点

  • 访问一个聚合对象的内容,但不暴露对象内部
  • 使用同一的接口遍历不同结构的数据集合
  • 遍历的同时,更改迭代器的所在的聚合对象会导致错误

优缺点/应用场景
优点

  • 添加新的聚合类和迭代器类都很方便,无需更改原有代码
  • 简化聚合类,支持以不同方法遍历一个聚合对象

缺点

  • 迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

应用场景

迭代器模式通常用于:对于集合内部结果常常变化各异,我们不想暴露其内部结构的话,但又响让客户代码透明底访问其中的元素,这种情况下我们可以使用迭代器模式。

  • 如jQ中$.each()方法就是
$.each([1,2],function(index,value){
    console.log(index,value)
})
  • 使用迭代器模式实现each
    let myEach = funciton(arr,cb){
      for(let i = 0, i<arr.length,i++){
          cb(i,arr[i])
      }
    }
    

实战演练:

// 使用迭代器模式实现jq的 each() 方法
let myIterator = function(arr, calllback) {
  for (let i = 0; i < arr.length; i++) {
    const ele = arr[i];
    calllback(i, ele, arr);
  }
};

// 常规迭代器拥有以下方法 next方法 、 hasNext方法
function Iterator(obj) {
  console.log(obj, obj.length);
  let current = 0,
    len = obj.length || 0;

  this.hasNext = function() {
    return current < len;
  };

  this.next = function() {
    return obj[current++];
  };

  this.reset = function() {
    current = 0;
  };
}

let iter = new Iterator([1, 2, 3]);
while (iter.hasNext()) {
  console.log(iter.next());
}
iter.reset()
console.log('重置');
while (iter.hasNext()) {
  console.log(iter.next());
}

装饰者模式

在不改变原类(基类)和继承情况下,包装一个对象实现一个具有原对象相似的功能的新对象(新类)

具备如下特点

  • 新添加的类,对象不会改变基类本身的对象
  • 修饰对象和原对象具有相似的接口风格
  • 修饰对象具有对原始对象的引用

优缺点/使用场景
优点

  • 修饰对象和原始对象能够独立开发,互相不耦合,修饰模式是继承的另一种替代方式

缺点

  • 当修饰对象过多的时候,会比较复杂

使用场景

  • 在基类的基础上拓展出更多类似基类接口的方法,比如,一杯普通的咖啡就是基类,在普通的咖啡中添加设置甜度,温度,用什么杯子这就是修饰对象

实战演练:

/* 基础案例 */
function Sale(price) {
  this.price = price || 100;
}

Sale.prototype.getPrice = function() {
  return this.price;
};

Sale.decorators = {};

Sale.decorators.country = {
  getPrice: function() {
    let price = this.user.getPrice();

    // 获取父对象的值

    price += (price * 5) / 100;

    return price;
  }
};

Sale.decorators.privince = {
  getPrice: function() {
    let price = this.user.getPrice();

    price += (price * 7) / 100;

    return price;
  }
};

Sale.decorators.money = {
  getPrice: function() {
    return '¥' + this.user.getPrice().toFixed(2);
  }
};

Sale.prototype.decorate = function(decorator) {
  let F = function() {},
    newobj,
    overrides = this.constructor.decorators[decorator];

  F.prototype = this;

  newobj = new F();

  newobj.user = F.prototype;

  for (let k in overrides) {
    if (overrides.hasOwnProperty(k)) {
      newobj[k] = overrides[k];
    }
  }

  return newobj;
};

let sale = new Sale(100);

sale = sale.decorate('country');

sale = sale.decorate('privince');

sale = sale.decorate('money');

console.log(sale.getPrice());

大体上 是在基类的基础上进行添油加醋的修饰

观察者模式(订阅/发布模式)

观察者模式也可以称为订阅/发布模式,观察者相当于订阅者,发布者相当于被观察者,核心是一个对象订阅另一个对象,浏览器中的事件都是一个观察者模式

观察者模式 vs 订阅/发布模式

观察者模式

一种一对多的模式,一个被观察者(主题对象)对应多个观察者,当主题对象发生变化的时候,会通知对应的所有观察者

观察者模式

订阅/发布模式

整体上和观察者模式差不多,处理方式上有出入,订阅者和发布者相互是不通信的,仅通过调度中心进行统一处理,发布者发布该事件到调度中心(并携带上下文)由调度中心统一调度订阅者注册到调度中心的处理代码
订阅/发布模式

优缺点和使用场景以及差异

差异

  • 观察者模式大多是同步的,订阅/发布大多数是异步的(由于使用的是消息队列的方法)
  • 观察者模式需要在单个应用程序地址中是实现,而发布/订阅更像交叉应用模式
  • 发布订阅模式中,组件是松散耦合的(双向解耦),正好与观察者模式是相反的(单向解耦)

优点

  • 可以一对多,并且程序便于拓展

缺点

  • 多个观察者对应一个订阅者,增加维护难度,并且会消耗时间
  • 发布订阅模式中,中心任务过重的时候,一旦崩溃,所有订阅者都会受到影响

案例

  • dom 的事件触发就是经典的观察者模式,vue中父子组件通信使用的emit 也是订阅/发布模式

实战演练
观察者模式

/* 观察者模式 */
class Subject {
  constructor() {
    // 订阅主题的观察者, subscriber
    this.subs = [];
  }

  subscribe(sub) {
    // 订阅
    this.subs.push(sub);
  }

  unsubscribe(sub) {
    // 取消订阅
    const index = this.subs.indexOf(sub);
    if (index > -1) {
      this.subs.splice(index, 1);
    }
  }

  fire() {
    // 主题变化,通知订阅主题的观察者
    this.subs.forEach(sub => {
      sub.notify();
    });
  }
}

class Observer {
  constructor(data) {
    this.data = data;
  }

  notify() {
    console.log(this.data);
  }
}

let subject = new Subject();
let ob1 = new Observer('hello');
let ob2 = new Observer('world');
subject.subscribe(ob1);
subject.subscribe(ob2);
subject.fire();

订阅/发布模式

/* 订阅/发布模式 */
class EventChannel {
  constructor() {
    // 主题
    this.subjects = {};
  }

  hasSubject(subject) {
    return this.subjects[subject] ? true : false;
  }

  /**
   * 订阅的主题
   * @param {String} subject 主题
   * @param {Function} callback 订阅者
   */
  on(subject, callback) {
    if (!this.hasSubject(subject)) {
      this.subjects[subject] = [];
    }
    this.subjects[subject].push(callback);
  }

  /**
   * 取消订阅
   */
  off(subject, callback) {
    if (!this.hasSubject(subject)) {
      return;
    }
    const callbackList = this.subjects[subject];
    const index = callbackList.indexOf(callback);
    if (index > -1) {
      callbackList.splice(index, 1);
    }
  }

  /**
   * 发布主题
   * @param {String} subject 主题
   * @param {Argument} data 参数
   */
  emit(subject, ...data) {
    if (!this.hasSubject(subject)) {
      return;
    }
    this.subjects[subject].forEach(callback => {
      callback(...data);
    });
  }
}

const channel = new EventChannel();

channel.on('update', function(data) {
  console.log(`update value: ${data}`);
});

channel.emit('update', 123);

代理模式(Proxy Pattern)

为其他对象提供一个代理,来控制这个对象的访问,代理是在客户端和真实对象中的一个媒介;简单来说,我的理解是将方法再包一层,过滤、处理后在进入真实对象,包的这层就是代理层

优缺点以及应用场景
优点

  • 拓展性比较强,开闭原则
  • 保护真实对象,职责逻辑清晰

缺点

  • 添加代理层,增加请求处理效率,并增加逻辑处理复杂度

应用场景

  • 我们吃午饭的两种方式 1.去餐厅吃;2.点外卖;后者就是代理模式,将去吃饭这个动作交给了外卖员

实战演练

// 定义午饭类 参数 菜名
let Lunch = function(greens) {
  this.greens = greens;
};

Lunch.prototype.getGreens = function() {
  return this.greens;
};

// 定义外卖小哥这个对象
let brother = {
  buy: function(lunch) {
    leo.buy(lunch.getGreens());
  }
};

// 定义我这个对象
let leo = {
  buy: function(greens) {
    console.log(`午饭吃${greens}`);
  }
};

// 叫外卖
brother.buy(new Lunch('青椒炒肉')); // 午饭吃青椒炒肉

相当于让别人拿着你实例对象去帮你做你想做的事情

中介者模式 (mediator Pattern)

用来降低多个对象和类之间的通信复杂度,促进形成松耦合提高可维护性,个人理解有些像,发布/订阅模式,各个子类都通过一个中间工具进行交互通信

https://mmbiz.qpic.cn/mmbiz_png/dy9CXeZLlCUKy4uM2OCicnCNYz7josduJD1gDRt1RImIXhubUuxG5ygkPV6ichXZSyZbnXlsCTHFfbxc3GdJzGRA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

优缺点以及应用场景

  • 使各个类之间进行解耦,但中介者缺变得臃肿了
  • 从一对多到一对一(虽然书中这么说 但从中介者的角度触发,感觉还是一对多?)

应用场景

  • 系统中对象之间存在比较复杂的引用关系,而且难以复用该对象
  • 需要生成最少的子类,实现一个中间类封装多个类中的行为的时候

实战演练

//  以下是使用中介者模式 一组选手的比赛结果,在playermiddle的中介类中处理不同选手的结果并最终输出结果
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);
};

// 将就用下这个 demo,这个函数当成中介者
const playerMiddle = (function() {
  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.win();
c.lose();

外观模式(Facade Pattern)

是一种简单又常用的模式,使得复杂的子系统接口提供一个更加高级的同一接口,方便对子系统接口的访问(个人理解就是对方法的封装,如util工具类)
优缺点以及应用场景

优点

  • 轻量级,减少对下层逻辑强的依赖
  • 提高灵活性、安全性(新手仅调用上层接口)

缺点

  • 不符合开闭原则,修改下层接口比较麻烦,不推荐继承和重写

应用场景

  • 为复杂的模块和子系统提供外界访问的模块
  • 子系统相对独立和稳定一致性比较高

实战演练

let util = {
  stopEvent: function() {
    // 其他
    if (typeof e.preventDefault === 'function') {
      e.preventDefault();
    }
    if (typeof e.stopPropagation === 'function') {
      e.stopPropagation();
    }

    // IE
    if (typeof e.returnValue === 'boolean') {
      e.returnValue = false;
    }
    if (typeof e.cancelBubble === 'boolean') {
      e.cancelBubble = true;
    }
  }
};

后记

设计模式是个很需要日常开发中基类和思考的一种思想吧,使用恰当能极大的提高我们的开发效率,以及编码风格,以上介绍的设计模式仅仅只是我所接触和认识到的,还有很多新的设计思路值得我们去发现,设计模式demo以发布到GitHub,有兴趣的小伙伴可以查阅

Bye-bye~

comments