JavaScript 设计模式之创建型设计模式

1,289 阅读13分钟

简单工厂模式

工厂模式是用来创建对象的一种最常用的设计模式。我们不需要暴露创建对象的具体逻辑,而是将逻辑都封装在一个函数中,那么这个函数将视为一个工厂。工厂模式可分为简单工厂工厂方法抽象工厂


简单工厂模式 又称为 静态工厂方法工厂函数,是由一个工厂对象(函数)用来创建一组相似的产品对象实例。

把需要的方法都封装在一个函数内,通过调用该函数返回我对应的对象。

实例化对象创建的 例如体育商品店购买体育器材,当我们购买一个篮球和及其介绍时,只需告诉售货员,售货员就会帮我们找到需要的东西。

function Bask(){
	this.name = "篮球"
}
Bask.prototype.getBask = function(){
	console.log("篮球队员需要5个");
}
function Football(){
	this.name = "足球";
}

Football.prototype.getFootball = function(){
	console.log("足球队员需要11个");
}
function Sort(type){
	switch(type){
        case "bask":
			return new Bask();
		break;
        case "foot":
			return new Football();
		break;
		default :
		console.log("只有篮球和足球");
	}
}
let f = Sort("bask");
f.name; // "篮球"
f.getBask() // 篮球队员需要5个

Sort() 就是这样一个工厂函数,给 Sort() 传入对应的参数就可以得到我们需要的对象了。
一个对象有时也可以代替许多类

简单工厂模式 中将类似的功能提取出来,不相似的针对处理,这点很像我们的继承的 寄生模式,但这里没有父类,所以不需要继承。只需要创建一个对象,然后通过对这个对象大量扩展属性和方法,并在最终的时候返回来。

"比如,想创建一些书,那么书都是有一些相似的地方发,比如目录、页码等。不相似的地方,比如书名、出版时间,书的类型等。对于创建的对象相似属性好处理,对于不同的属性就要针对性的处理了。比如我们将不同的属性作为参数传递进来处理。"

通过创建一个新对象然后增强其属性和功能实现

function createBook(name,time,type){
	let obj = {};
	obj.name = name;
	obj.time = time;
	if(type == "js"){
		obj.type = "js 书籍";
	}else if (type == "css"){
		obj.type = "css 书籍"
	}else {
		obj.type = type;
	}
	obj.getName = function(){
		console.log(obj.name);
	}
	obj.getType = function(){
		console.log(obj.type)
	}
	return obj;
}
let c = createBook("JavaScript 高级程序第三版","2019","js");
c.getType(); // js 书籍

实例化创建的和自创建对象的区别 通过类实例化创建的对象,如果这些类都继承同一个父类,那么他们父类的原型的方法时可以共用的。自创建方式创建的对象都是一个新的个体,因此他们的方法是不能共用的。

对于一些重复性的需求我们都可以采用工厂模式来创建一些对象,可以让这些对象共用一些资源也有私有的资源。不过对于简单工厂模式,它的使用场合通常也就是限制在创建单一的对象。


工厂方法模式


工厂方法模式的本意是说`将实际创建的对象工作推迟到子类中`。这样核心类就成了抽象类,但是在 Javascript 中没有传统创建抽象类那样的方式去实现抽象类,所以在 JavaScript 中实现工厂方法模式我们只需要参考它的核心思想就可以了。把`工厂方法看作是一个实例化对象的工厂类`。

在简单工厂模式中每次添加构造函数方法的时候都需要修改 2 个地方 ( 添加构造函数类和修改工厂函数 ),现在改用 工厂方法模式 ,把工厂方法看作是一个实例化对象的工厂,只负责把要添加的类添加到这个工厂就可以了。

以上面的 书籍 代码为例

function CreateBook(type,name,time){
	if(this instanceof CreateBook){
		return new this[type](name,time)
	}else{
		return new CreateBook(type,name,time)
	}
}
CreateBook.prototype = {
    constructor: CreateBook,
    getName:function(name,time){
        console.log(name,time);
    },
    getType: function(name,time){
        console.log(name,time);
    }
}

直接把要添加的类写在 CreateBook 这个工厂类的原型里面就可以避免修改很多操作了。

对于创建很多类对象,简单工厂模式就不适合用了,这是简单工厂模式的应用局限,当前这也正是 工厂方法 的价值所在,通过 工厂方法模式 可以轻松创建多个类的实例对象,这样工厂方法在创建对象的方式也避免了使用者和对象类之间的耦合度,用户不需要关心创建该对象的具体类,只需要调用工厂方法即可。

在了解抽象工厂模式之前,我们先了解一下什么是抽象类。 ** 抽象类** 在 js 中还保留着一个 abstract 保留字,目前来说还不能像传统面向对象那样去创建一个抽象类。抽象类是一种声明但不能使用的类,当使用就会报错。在 js 中,我们可以用类来模拟一个抽象类,并且在类的方法中手动的抛出错误。

function Car(){};
Car.prototype = {
	constructor : Car,
	getPrice:function(){
		return new Error("抽象类的方法不能被调用!");
	}
	getSeed:function(){
		return new Error("抽象类的方法不能被调用!");
	}
}

Car 类其实什么也没做,创建的时候没有任何属性,原型中的方法也不能使用。但是在继承上很有用,因为定义了一种类,并且定义了该类所必备的方法,如果子类中没有重写这些方法就会报错,这点很重要,因为在一些大型的应用中,总有一些子类继承另一些父类,这些父类经常会定义一些必要的方法,却没有具体实现,比如 Car 类一样,继承的子类如果没有自己定义所必备的方法就会调用父类的,这时如果父类能够提供一个友好的提示,那么对于忘记重写子类方法的这些错误遗漏是很有必要的, 这也是抽象类的一个作用,定义一个产品簇,并声明一些所必备的方法,如果子类没有声明一些必备的方法重写就会报错

在 ES6 中也没有实现abstract , 但要比我们上面那种写法简约很多。ES6 中采用 new.target 来模拟出抽象类,new.target 指向直接被 new 执行的函数,我们对 new.target 进行判断,如果指向了该类则抛出错误表示这是一个抽象类。

class User{
	constructor(){
		if(new.target === User){
			console.log("抽象类不能被实例化");
		}
	}
}
class U extends User{
	constructor(name,age){
		super();
	}
	getname(){
		console.log(this.name)
	}
}
let a = new User(); // 抽象类不能被实例化
let a1 = new U("kk",2);
a1.name // "kk"

抽象工厂模式


在 js 中抽象工厂模式不用来创建具体对象,因为抽象类中定义的方法都是显性地定义了一些功能,但没有具体的实现,而一个对象是要有具体的一套完整的功能,所以用抽象类创建的对象也是抽象类,因此不能使用它来创建一个真实的对象。

所以,一般用它作为父类来创建一些子类。

function VehicleFactory(subType, superType) {
     // VehicleFactory[superType] 对象获取属性
     if (typeof VehicleFactory[superType] === "function") {
         function F() { };
         F.prototype = new VehicleFactory[superType]();
         subType.prototype = new F();
         subType.constructor = subType;
     } else {
         throw new Error("未创建该抽象类")
     }
 }
 VehicleFactory.Car = function () {
     this.type = "car";
 }
 VehicleFactory.Car.prototype = {
     getPrice: function () {
         return new Error("抽象方法不能使用!")
     }
 }

 //汽车子类
 function BMW(price) {
     this.price = price;
 }
 VehicleFactory(BMW, 'Car');
 BMW.prototype = {
     getPrice: function () {
         console.log(this.price);
     }
 }
 let b = new BMW("小汽车滴滴滴...");
 b.getPrice(); //小汽车滴滴滴...

抽象工厂 VehicleFactory 其实是一个实现子类继承父类(创建子类)的方法,在该方法中需要通过传递 子类和要继承的父类(抽象类) 的名称,并且在抽象工厂方法中增加了对抽象类是否存在的判断,存在则将子类继承父类,继承父类的过程中需要对 过渡类的原型继承时,我们不是继承父类的原型,而是通过 new 关键字复制父类的一个实例,这是因为 过渡类不应该仅仅是继承父类的原型方法,还要继承父类的对象属性,所以要通过 new 关键字将父类的构造函数执行一遍来复制构造函数中的属性和方法

对抽象工厂添加抽象类也很特殊,因为抽象工厂是个方法不需要实例化,所以只有一份, 因此直接给抽象工厂添加类的属性就可以了。于是我们就可以通过点(.)语法在抽象工厂上添加了小汽车簇抽象类(Car)。

抽象工厂模式创建出的不是一个真实的对象实例,而是一个类簇,制定了类的结构,这也就是区别简单工厂模式创建一个单一对象,工厂方法模式创建多类对象。


创建者模式

工厂模式 主要就是为创建实例对象或类簇(抽象工厂),不在乎过程是啥,只关心最后返回的对象。

创建者模式概念
在创建对象时比较复杂,但更关心的是创建对象的过程,根据需求分解成多个对象,最后在拼接到一个对象返回。

比如下面是一个专门负责招聘的需求功能

// 创建一个 人类方法
function Human(param) {
    this.skill = param && param.skill || "保密";
    this.hobby = param && param.hobby || "保密";
}
// 提供原型方法
Human.prototype = {
    getSkill: function () {
        return this.skill;
    },
    getHobby: function () {
        return this.hobby;
    }
}
// 创建名字
function Named(name) {
    this.name = name;
}
// 创建工作职位
function Work(work) {
    let that = this;
    switch (work) {
        case 'Java':
            that.work = "Java工程师";
            that.workTxt = "每天沉迷 Java 不可自拔!";
            break;
        case 'JavaScript':
            that.work = "JS 工程师";
            that.workTxt = "每天沉迷 JS 不可自拔!";
            break;
        case 'UI':
            that.work = "UI 设计师";
            that.workTxt = "设计是一种艺术!";
            break;
        case 'PM':
            that.work = "产品经理";
            that.workTxt = "每天都在做需求!";
            break;
        default:
            that.work = work;
            that.workTxt = "对不起,不清楚你的分类";
    }
}
// 修改工作职位
Work.prototype.changeWork = function(work){
    this.work = work;
}
// 修改工作描述
Work.prototype.changeWorkTxt = function(txt){
    this.workTxt = txt;
}
// 重点,创建一个面试者,在这个阶段进行拼接
function Person (name,work){
    let p = new Human();
    p.w = new Work(work);
    p.name = new Named(name);
    return p;
}
let p1 = newPerson("xiaoming", "Java");
console.log(p1.w.work); // "Java工程师"
console.log(p1.name.name);// "xiaoming"
p1.w.changeWork("UI");
console.log(p1.w.work);// "UI"
console.log(p1.getSkill()); //保密

Person 就是一个创建者函数,在该函数内我们把 3 个类组合调用,就可以创建出一个完整的应聘者对象了。

建造者模式中,我们更关心创建对象的过程。把功能拆分在拼接得到一个完整去对象。主要针对复杂业务的解耦。

原型模式

原型模式 将原型对象指向创建对象的类,使这些类共享原型对象的属性和方法。

这是基于 JS 原型链实现对象之间的继承,这种继承是一种属性或方法的共享,而不是对属性和方法的复制。

在创建的类中,存在基类,其定义的属性和方法能被子类继承。

原型模式将可复用的、可共享的、耗时较长的从基类中提出来放在基类的原型中,然后子类通过组合继承或寄生组合式继承把属性和方法继承下来,对于子类中那些需要重写的方法进行重写,这样子类创建的对象既具有子类的属性和方法同时也共享了基类的原型方法。

例子
比如页面中经常见到的焦点图,焦点图的切换效果都是多变的,有左右切换的,有上下切换的还有渐隐切换的等等。因此我们应该抽象出一个基类,根据不同需求来重写继承的属性和方法。

代码

function LoopImage(imgArr,container){
	this.imgArr = imgArr;
	this.container = container;
	this.createImg = function(){} // 创建轮播图片
	this.changeImg = function(){} // 切换下一张图片
}
// 上下切换效果
function SlideLoopImg(imgArr,container){
	LoopImage.call(this,imgArr,container);
	this.changeImg = function(){
		console.log("SlideLoopImg changeImg");
	}
}
// 渐隐切换效果
function FadeLoopImg(imgArr,container,arrow){
	LoopImage.call(this,imgArr,container);
	// 切换箭头私有变量
	this.arrow = arrow;
	this.changeImg = function(){
		console.log("FadeLoopImg changeImg");
	}
}

// 实例化一个渐隐切换效果图片类
let fadeImg = new FadeLoopImg(["01.jpg","02.jpg"],"slide",["left.jpg","right.jpg"]);
fadeImg.changeImg(); // FadeLoopImg changeImg

如上代码还存在一些问题,首先看我们的基类 LoopImage,作为基类是要被子类继承的,那么此时将属性和方法都写在基类的构造函数里就会有一些问题,比如每次子类继承父类都要重新创建一次,又或者父类中的构造函数中存在很多耗时长的逻辑,亦或者每次初始化都是一些重复性的东西,对性能的消耗很多。

所以我们需要一种共享机制,这样每当创建基类的时候,对于一些简单或者差异化的东西放在构造函数内,对于可重复,耗时长的逻辑放在基类的原型中。这样就可以避免损耗性能。

function LoopImage(imgArr,container){
	this.imgArr = imgArr;
	this.container = container;
}
LoopImage.prototype.createImg = function(){} // 创建轮播图片
LoopImage.prototype.changeImg = function(){} // 切换下一张图片
// 上下切换效果
function SlideLoopImg(imgArr,container){
	LoopImage.call(this,imgArr,container);
}
SlideLoopImg.prototype = new LoopImage();
SlideLoopImg.prototype.changeImg = function(){
	console.log("SlideLoopImg changeImg");
}
// 渐隐切换效果
function FadeLoopImg(imgArr,container,arrow){
	LoopImage.call(this,imgArr,container);
	// 切换箭头私有变量
	this.arrow = arrow;
}
FadeLoopImg.prototype = new LoopImage();
FadeLoopImg.prototype.changeImg = function(){
	console.log("FadeLoopImg changeImg");
}

// 实例化一个渐隐切换效果图片类
let fadeImg = new FadeLoopImg(["01.jpg","02.jpg"],"slide",["left.jpg","right.jpg"]);
fadeImg.changeImg(); // FadeLoopImg changeImg

原型对象是一个共享的对象,不管是父类的实例对象还是子类的继承,都是靠一个指针引用的。所以在任何时候对基类或者子类的原型进行拓展,所有实例化的对象或者类都能获取到这些方法。

单例模式

又称为单体模式,只允许实例化一次的对象类,有时候我们也用一个对象来规划一个命名空间,以便管理对象上的属性和方法。

命名空间 也有人称为名称空间,用来约束每个人定义的变量以避免所有不同的人定义的变量存在重复导致冲突的。

单例模式例子

var Ming = {
	g:function(id){
		return document.getElementById(id);
	},
	c:function(id,key,value){
		this.g(id).style[key] = value;
	}
}

在单例模式中想要使用定义的方法一定要加上命名空间 Ming,所以在上面代码中的 c 方法中的 g 函数调用的时候要改成 Ming.g。由于 g 方法和 c 方法都在 Ming 中,也就是说这 2 个方法都是单例对象 Ming 的。而对象中的 this 指向当前对象。所以我们也可以像上面代码那样直接使用 this.g 。


创建代码库

单例模式 除了定义命名空间的作用之外还有一个作用就是通过单例模式来管理代码库中的各个模块。

比如我们以后在写自己的小型方法库时可以用单例模式来规范自己代码库的各个模块。

var A={
	Util:{
		Util_1:function(){},
		Util_2:function(){}
	},
	Tool:{
		Tool_1:function(){},
		Tool_2:function(){}
	},
	Ajax:{
		getName:function(){},
		postName:function(){}
	}
	// ...
}

使用模块方法时

A.Util.Util_1();
A.Ajax.getName();
// ....

惰性单例
惰性单例指在需要的时候才会创建,也称为延迟创建。 惰性单例模式,用到时才创建,再次使用是不需要在创建的。

var Lazy = (function(){
	var instance = null;
	// 单例
	function Sligle(){
		/*这里可以定义私有属性和方法*/
		return {
			p:function(){},
			pv:"1.0"
		}
	}
	// 获取单例对象接口
	return (function(){
		// 如果还没有创建单例才开始创建
		if(!instance ){
			instance = Sligle();
		}
		// 返回单例
		return instance;
	})
})()
Lazy().p // 通过 Lazy对象 获取内部创建的单例模式对象

参考:JavaScript 设计模式