该系列参考自 《JavaScript 设计模式》

以及 汤姆大叔的博文 深入理解 JavaScript 系列

前言

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式
  • 原型模式
  • 单例模式

一. 简单工厂模式

简单工厂模式的概念就是创建对象,不仅如此,简单工厂模式还可以用来创建相似的对象。

场景

如果你想创建一些书,那么这些书都有一些相似的地方,比如目录、页码等。也有很多不同的地方, 如书名、出版时间、书的类型等,对于创建的对象相似的属性当然好处理,对于不同的属性就要有针对的修改处理了。

//工厂模式
function createBook(name, time, type) {
  //创建一个对象,并且对对象拓展属性和方法
  var o = new Object();
  o.name = name;
  o.time = time;
  o.type = type;
  o.getName = function() {
    console.log(this.name);
  };

  //将对象返回
  return o;
}

var book1 = createBook('js book', '2017/11/16', 'JS');
var book2 = createBook('css book', '2017/11/13', 'CSS');

book1.getName();
book2.getName();
var Basketball = function() {
  this.info = '美国篮球';
};
Basketball.prototype = {
  constructor: Basketball,
  getMember: function() {
    console.log('每队需要5个成员');
  },
  getBallSize: function() {
    console.log('这个篮球还是很大的');
  }
};
var Football = function() {
  this.info = '这是足球';
};
Football.prototype = {
  constructor: Football,
  getMember: function() {
    console.log('足球每队需要十一个人');
  },
  getBallSize: function() {
    console.log('足球我不喜欢');
  }
};
var Tennis = function() {
  this.info = '网球貌似现在还是蛮流行的';
};
Tennis.prototype = {
  constructor: Tennis,
  getMember: function() {
    console.log('一对一,二对二咯');
  },
  getBallSize: function() {
    console.log('网球还是比较小的');
  }
};

//球类工厂
var sportsFactory = function(name) {
  switch (name) {
    case 'NBA':
      return new Basketball();
      break;
    case 'wordCup':
      return new Football();
      break;
    default:
      return new Tennis();
  }
};

这种简单工厂模式非常的像寄生式继承,只不过这里 o 没有继承任何对象和类。

简单工厂的用途就是创建对象,或者创建相似的对象。

二. 工厂方法模式

工厂方法模式本意是将实际创建对象的工作推迟到子类当中。这样核心类就成为了抽象类。

前言

在之前,我们介绍过简单工厂设计模式,简单工厂设计模式存在唯一的工厂类,它的优点是所有产品类的实例化集中管理,便于理解。当产品数量较少,且不会经常发生变化时,我们当然可以直接使用简单工厂模式,但是有的时候,需求是在时刻变化的,产品类也可能随之增加,如果使用简单工厂模式,就避免不了去修改工厂类的代码。要解决这个问题,就得使用今天所讲的,工厂方法模式。

基本概念

工厂方法模式:不再有一个唯一的工厂类就创建产品,而是将不同的产品交给对应的工厂子类去实现。每个产品由负责生产的子工厂来创造。如果添加新的产品,需要做的是添加新的子工厂和产品,而不需要修改其他的工厂代码。

工厂方法模式主要有三种类组成:

  • 抽象工厂类:负责定义创建产品的公共接口
  • 产品子工厂:继承抽象工厂类,实现抽象工厂类提供的接口
  • 每一种产品各自的产品类

安全模式类

安全模式类就是可以屏蔽对类的错误使用而造成的后果。说白了,就是在构造函数开始时先判断当前对象 this 指向是不是类。

var Demo = function() {
  if (!(this instanceof Demo)) {
    return new Demo();
  }
};
Demo.prototype.show = function() {
  console.log('show');
};

var d = Demo();
d.show();

采用工厂方法模式

简单工厂模式仅仅适用于创建比较少的对象,如果需要创建多个类,并且会经常修改,像我们之前说的简单工厂的方法就不是很实用了,因为如果我要多添加一个类,就需要修改两个地方,所以这里我们采用工厂方法模式

var Factory = function(type, content) {
  if (this instanceof Factory) {
    var temp = new this[type](content);
  } else {
    return new Factory(type, content);
  }
};
//在工厂原型中设置创建所有类型数据对象的基类
Factory.prototype = {
  constructor: Factory,
  Java: function(content) {
    //...
  },
  JavaScript: function(content) {
    //...
  },
  UI: function(content) {
    this.content = content;
    (function(content) {
      var div = document.createElement('div');
      div.innerHTML = content;
      div.style.border = '1px solid red';
      document.getElementById(content).appendChild(div);
    })(content);
  }
};

如上,我们就可以创建多个类了

var data = [
  {
    type: 'JavaScript',
    content: 'Javascript还是很重要的'
  },
  {
    type: 'Java',
    content: 'Java培训哪家强'
  },
  {
    type: 'UI',
    content: 'UI...'
  }
];

for (var i = 0, length = data.length; i++; i < data.length) {
  Factory(data[i].type, data[i].content);
}

三. 抽象工厂模式

通过对类的工厂抽象使其业务用于对产品类簇的创建,而不负责创建某一类产品的实例。也就是说我们要创建一个抽象类。这也是面向对象的开发语言中一种很常见的开发模式。

场景

var Car = function() {};
Car.prototype.getPrice = function() {
  return new Error('抽象方法不能调用,需自行实现');
};
Car.prototype.getSpeed = function() {
  return new Error('抽象方法不能调用,需自行实现');
};

由于 JavaScript 在没有 abstract 的具体实现,所以我们需要如上手动实现,也即是在创建这个类的时候,要求使用这些方法,我们需要手动去重写它,而不能继承使用。因为 在大型的应用中,总有一些子类去继承一些父类,这些父类经常会定义一些必要的方法,但是不会具体的去实现,会去要求子类自行实现。如果子类没有重写这些方法而去调用他,就会报错。

// 抽象工厂方法
var VehicleFactory = function(subType, superType) {
  //判断抽象工厂中是否有该抽象类
  if (typeof VehicleFactory[superType] === 'function') {
    //缓存类
    function F() {}
    //继承父类属性和方法
    F.prototype = new VehicleFactory[superType]();
    //将子类的constructor指向子类
    subType.constructor = subType;
    //子类原型继承父类
    subType.prototype = new F();
  } else {
    return new Error('未创建该抽象类');
  }
};

//小汽车抽象类
VehicleFactory.Car = function() {
  this.type = 'car';
};
VehicleFactory.Car.prototype = {
  getPrice: function() {
    return new Error('抽象方法不能调用');
  },
  getSpeed: function() {
    return new Error('抽象方法不能调用');
  }
};
//公共汽车抽象类
VehicleFactory.Bus = function() {
  this.type = 'Bus';
};
VehicleFactory.Bus.prototype = {
  getPrice: function() {
    return new Error('抽象方法不能调用');
  },
  getSpeed: function() {
    return new Error('抽象方法不能调用');
  }
};
//大卡车抽象类
VehicleFactory.Trunk = function() {
  this.type = 'Trunk';
};
VehicleFactory.Trunk.prototype = {
  getPrice: function() {
    return new Error('抽象方法不能调用');
  },
  getSpeed: function() {
    return new Error('抽象方法不能调用');
  }
};
//子类
var BMW = function(price, speed) {
  this.price = price;
  this.speed = speed;
};
VehicleFactory(BMW, 'Car');
BMW.prototype.getPrice = function() {
  return this.price;
};
BMW.prototype.getSpeed = function() {
  return this.speed;
};

//...

var three = new BMW('35w', '200');
three.getSpeed();
console.log(three.getPrice());

我们可以看出,抽象工厂其实是实现子类继承父类的方法,在这个方法里,我们需要传递子类以及需要被继承的父类的名称,并且在抽象工厂方法中,又增加了一次对抽象类存在性的一次判断,然后通过寄生式继承,在继承中我们是通过 new 关键字复制了父类的一个实例,因为我们不仅仅需要继承父类原型上的方法,还需要继承父类的属性。所以通过 new 关键字将父类的构造函数执行一遍来复制父类构造函数中的属性和方法。

通过抽象工厂,我们就能知道每一个子类到底是哪一种类别了。

同时注意,抽象类中定义的方法这是显示定义一些功能,但是没有具体的实现,而一个对象是应该具备一套完整的功能的。所以用抽象类创建的对象当然也是抽象的。所以我们还不能直接用它来创建类的实例。

四. 建造者模式

将一个复杂对象的构建层和表示层相分离,同样的构建过程可以采用不同的表示。

场景

工厂模式主要是用来创建对象的实例(简单工厂模式,工厂方法模式)或者是类簇(抽象工厂模式),关心的是最终的产出是什么,所以工厂模式我们得到的是对象的实例或者对象的类簇。然而建造者模式在创建对象的过程中则更为复杂一些。虽然目的也是为了创建对象,但是更关心的创建对象的整个过程或者说是每一个细节。

比如创建一个人,我们创建的结果不仅仅是得到一个人的实例,还要关注创建人的时候,这个人是男是女,穿什么衣服带什么帽子等等。

var Human = function(param) {
  this.skill = (param && param.skill) || '保密';
  this.hobby = (param && param.hobby) || '保密';
};
Human.prototype = {
  constructor: Human,
  getSill: function() {
    return this.skill;
  },
  getHobby: function() {
    return this.hobby;
  }
};
var Name = function(name) {
  var that = this;
  (function(name, that) {
    this.wholeName = name;
    if (name.indexOf(' ') > -1) {
      that.firstName = name.slice(0, name.indexOf(' '));
      that.secondName = name.slice(name.indexOf(' '));
    }
  })(name, that);
};
var Work = function(work) {
  var that = this;
  (function(work, that) {
    switch (work) {
      case 'code':
        that.work = '工程师';
        that.wordDesc = '代码使我快乐';
        break;
      case 'UE':
        that.work = '设计师';
        that.wordDesc = '设计更似艺术';
        break;
      default:
        that.work = work;
        that.wordDesc = '对不起,我们还不清楚你所选择职位的相关描述';
    }
  })(work, that);
};

//更换期望职位以及描述
Work.prototype.changeWork = function(work) {
  this.work = work;
};

Work.prototype.changeDesc = function(desc) {
  this.wordDesc = desc;
};

//创建一个应聘者
var Person = function(name, work) {
  var _person = new Human();
  _person.name = new Name(name);
  _person.work = new Work(work);
  return _person;
};

var person = new Person('Liusixin', 'code');
console.log(person.skill);
console.log(person.hobby);
console.info(person.work);
person.work.changeDesc('一撸代码就疯狂');
console.info(person.work);

五. 原型模式

用原型实例指向创建对象的类,适用于创建新的对象的类共享原型对象的属性和方法。这种继承是一种基于对属性和方法的共享而不是复制。

场景

在创建的类中,存在基类,起定义的方法和属性能够被子类所继承和使用。

原型模式就是将可复用的、可共享的、消耗大的从基类中提取出来然后放到原型中,然后子类通过组合继承或者寄生组合式继承将方法和属性继承下来。子类中对于那些需要重写的方法进行重写,这样,子类 创建的对象既有子类的属性和方法也共享着基类的属性和方法。

示例

拿网页中轮播图举例,有的是渐变效果,有的是滚动,有的带有箭头。。。所以这里我们可以创建一个基类,轮播图,然后再根据不同的需求再去修改。

//图片轮播图类
var LoopImages = function(imgArr, container) {
  this.imageArr = imgArr; //轮播图片数组
  this.container = container; //轮播图片容器
  this.createImage = function() {}; //创建轮播图
  this.changeImage = function() {}; //轮播图切换
};
//上下切换
var SlideLoopImage = function(imgArr, container) {
  //构造函数继承图片轮播类
  LoopImages.call(this, imgArr, container);

  //重写继承的图片切换方法
  this.changeImage = function() {
    console.log('上下切换的方式');
  };
};
//渐隐切换
var FadeLoopImg = function(imgArr, container, arrow) {
  LoopImages.call(this, imgArr, container);
  //切换箭头私有变量
  this.arrow = arrow;
  this.changeImage = function() {
    console.log('渐隐的切换方式');
  };
};

//实例化一个
var fadeImg = new FadeLoopImg(['01.jpg', '02.jpg', '03.jpg'], 'slide', [
  'left.jpg',
  'right.jpg'
]);

但是如上的写法 ,其实还有一种更加优化的方法。首先看基类,作为基类是要被子类继承的,那么此时将属性和方法都写到基类的构造函数里会有一些问题。 比如每一次创建子类继承都要创建一次父类,如果父类的构造函数的创建过程中存在很多耗时较长的逻辑,这样的话性能消耗还是蛮大的。为了提高性能,我们可以使用共享机制。

对于每一次创建的一些简单而又差异化的属性我们可以放到构造函数中,而把一些消耗资源比较大的方法放到基类的原型中。这样避免很多不必要的消耗。

var LoopImages = function(imgArr, container) {
  this.imageArr = imgArr;
  this.container = container;
};
LoopImages.prototype = {
  crateImage: function() {
    console.log('创建轮播图方法');
  },
  changeImage: function() {
    console.log('图片切换方法');
  }
};
//上下切换
var SlideloopImg = function(imgArr, container) {
  LoopImages.call(this, imgArr, container);
};
SlideloopImg.prototype = new LoopImages();

SlideloopImg.prototype.changeImage = function() {
  console.log('上下切换的方式');
};

var FadeLoopImg = function(imgArr, container, arrow) {
  this.arrow = arrow;
  LoopImages.call(this, imgArr, container);
};
FadeLoopImg.prototype = new LoopImages();

FadeLoopImg.prototype.changeImage = function() {
  console.log('渐隐切换方式');
};

六. 单例模式

单例模式又称为单体模式,其实就是只允许实例化一个对象,有时我们也可以用一个对象类规划命名空间,仅仅有条的管理对象的属性和方法。

//命名空间管理
var obj = {
  g: function(id) {
    return document.getElementById(id);
  },
  css: function(id, key, value) {
    this.g(id).style[key] = value;
  }
  //...
};
//模块分明
var A = {
  Util: {
    util_method1: function() {},
    util_method2: function() {}
  },
  Tool: {
    tool_method1: function() {},
    tool_method2: function() {}
  }
  //...
};
//惰性单例
var LazySingle = (function() {
  //单例实例引用
  var _instance = null;
  //单例
  function Single() {
    return {
      publicMethod: function() {},
      publicProperty: '1.0'
    };
  }
  //获取单例接口
  return function() {
    if (!_instance) {
      _instance = Single();
    }
    //返回单例
    return _instance;
  };
})();