简单工厂模式
顾名思义,通过工厂对象来创建产品对象类的实例,即用来创建同一类或功能相似的对象。
例如:
- 方法一:页面中常用的弹窗,常常分为警示框、提示框、确认框等,对于其中相似的部分我们可以提取出来,如传入文字,弹框展示函数等,代码参考如下:
function createPop (type, text) {
// 创建一个对象,并对对象拓展属性和方法
var 0 = new Object();
o.content = text;
o.show = function () {
// 弹框展示函数
}
if (type === 'alert') {
// 警示框差异部分
}
if (type === 'prompt') {
// 提示框差异部分
}
if (type === 'confirm') {
// 确认框差异部分
}
// 将对象返回
return o;
}
// 创建警示框
var userNameAlert = createPop('alert', '用户名只能是16位及以下字母或数字组合');
- 方法二:创建球类方法
// 篮球基类
var Basketball = function () {
this.intro = '篮球盛行于美国';
}
Basketball.prototype = {
getMember: function () {
console.log('每个队伍需要5名队员');
},
getBallSize: function () {
console.log('篮球很大');
}
}
// 足球基类
var Football = function () {
this.intro = '足球在世界范围内很盛行';
}
Football.prototype = {
getMember: function () {
console.log('每个队伍需要11名队员');
},
getBallSize: function () {
console.log('足球很大');
}
}
// 网球基类
var Tennis = function () {
this.intro = '每年有很多网球系列赛';
}
Tennis.prototype = {
getMember: function () {
console.log('每个队伍需要1名队员');
},
getBallSize: function () {
console.log('网球很小');
}
}
// 运动工厂
var SportFactory = function (name) {
switch (name) {
case 'NBA':
return new Basketball();
case 'wordCup':
return new Football();
case 'FrenchOpen':
return new Tennis();
}
}
总结:
第一种方法是通过创建一个新的对象然后包装增强其属性和功能来实现,创建的对象丢失一个新的个体,他们的方法不能共用;
第二种方法是通过类实例化对象创建的,在这种方法中,创建的对象如果创建的类继承自同一个父类,则他们原型上的方法是可以共用的。
可以根据实际情况进行选择。
缺点:
- 如果在接到类似需求,需要既修改工厂方法,又要创建基类,两方面都要更改;
- 不适合创建多类对象
工厂模式
特点:通过对产品类的抽象,使其用于创建多类产品的实例。将实际创建对象的工作推迟到子类中,这样核心类就成了抽象类。避免了使用者和对象类之间的耦合,用户不必关心创建该对象的具体类,只需调用工厂方法即可。
首先,我们来了解一下安全模式:
var Demo = function () {
if (!(this instanceof Demo)) {
return new Demo ();
}
}
Demo.prototype = {
show: function () {
console.log('成功获取!');
}
}
var d = new Demo();
d.show(); // 成功获取!
var d = Demo();
d.show(); // 成功获取
这里我们看到,上面在工厂方法中加入判断当前对象是不是类(Demo),如果不是类,此时的this指向Window,则通过new关键字来创建新对象并将其返回;如果是直接返回该对象。能有效避免var d=Demo();
d.show()调用时的报错出现。
// 创建工厂模式
var Factory = function (type, content) {
if (this instanceof Factory) {
var s = new this[type](content);
return s;
} else {
return new Factory(type, content);
}
}
// 工厂原型中设置创建所有类型数据对象的基类
Factory.prototype = {
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('container').appendChild(div);
})(content);
},
php: function (content) {
// ......
}
}
在这里我们看到,如果以后接到相似需求,需要添加方法,直接修改Factory工厂方法就好,不在需要重新创建基类。
抽象工厂模式
通过对类的工厂抽象使其业务用于对产品类簇的创建,而不是负责创建某一类产品的实例。
关于抽象类:
抽象类是一种声明但是不能使用的类,当你使用时就会报错。即创建一个类,创建时没有属性,原型prototype上上网方法也不能使用,否则会报错,但是在继承上却是有用的,因为定义了一个类,并定义了该类所必备的方法,如果子类中没有重写这个方法,那么调用时能在父类上找到该方法并报错。这个特点在我们在一些大型应用中会用到,即子类去继承一些父类,父类常定义一些必要的方法,但是没有具体实现,那么一旦子类创建一个对象,该对象总是应该具备一些必要方法的,但是如果必要方法从父类中继承过来而没有被重写实现,则会找到原型链上的方法,从而报错。这事实上对于忘记重写子类是一种提示。
即:抽象类的作用是定义一个产品簇,并声明一些必备的方法,如果子类中没有重写就会报错。
注:JavaScript中存在保留字abstract,所以目前不能直接创建一个抽象类,但是可以在类的方法中手动抛出错误来模拟抽象类。
例如:
// 汽车抽象类,当使用其实例对象的方法是会抛出错误
var Car =- function () {};
Car.prototype = {
getPrice: function () {
return new Error('抽象方法不能调用');
},
getSpeed: function () {
return new Error('抽象方法不能调用');
}
}
由于抽象类只是显性的定义一些功能,但是没有具体实现,而一个对象是要有一套完整功能的,所以我们不能用它来创建一个真实的对象。一般情况下将其作为父类,创建子类,可以通过子类创建真实的对象。具体例子如下:
// 抽象工厂方法
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 {
throw 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 ('抽象方法不能调用');
},
getPassengerNum: function () {
return new Error ('抽象方法不能调用');
}
}
// 货车抽象类
VehicleFactory.Truck = function () {
this.type = 'truck';
}
VehicleFactory.Truck.prototype = {
getPrice: function () {
return new Error ('抽象方法不能调用');
},
getTrainload: function () {
return new Error ('抽象方法不能调用');
}
}
上面的例子中增加了对抽象类存在性的判断,如果存在,子类经继承父类的方法,这里我们看到:在继承父类的过程中,在对过渡类原型继承的时候,这里不是继承父类原型,而是通过new关键字复制父类的一个实例,这样做是因为过渡类不仅继承父类原型方法,还继承父类的对象属性。
对抽象工厂添加抽象类很特殊。因为抽象工厂方法不需要实例化,因而只需要一份,所以直接为抽象工厂添加类的属性即可,在上面的例子中是通过点语法在抽象工厂上添加了三个汽车簇抽象类Car、Bus、Truck。
使用方法:
// 宝马汽车子类
var BMW = function (price, speed) {
this.price = price;
this.speed = speed;
}
// 抽象工厂实现对Car抽象类的继承
VehicleFactory (BMW, 'Car');
BMW.prototype.getPrice = function () {
return this.price;
}
BMW.prototype.getSpeed = function () {
return this.speed;
}
// 宇通汽车子类
var YUTONG = function (price, passenger) {
this.price = price;
this.passenger = passenger;
}
// 抽象工厂实现对Bus抽象类的继承
VehicleFatory(YUTONG, 'Bus');
YUTONG.prototype.getPrice = function () {
return this.price;
}
YUTONG.prototype.getPassengerNum = function () {
return this.passenger;
}
// 奔驰汽车子类
var BenzTruck = function (price, trainLoad) {
this.price = price;
this.trainLoad= trainLoad;
}
// 抽象工厂实现对Truck抽象类的继承
VehicleFatory(BenzTruck, 'Truck');
BenzTruck.prototype.getPrice = function () {
return this.price;
}
BenzTruck.prototype.gettrainLoad = function () {
return this.trainLoad;
}
子类均可被实例化,如:
var truck = new BenzTruck(1000000, 1000);
console.log(truck.getPrice()); // 1000000
console.log(truck.type); // truck
缺点:
- JavaScript中不支持抽象化创建与虚拟方法,导致这种模式不能像其他面向对象语言中应用的那么广泛。
- 对于通用的方法如getPrice,不能复用。
建造者模式
将一个复杂对象的构建层与其表现层相互分离,同样的构建过程可采用不同的表示。
对比
工厂模式主要是为了创建对象实例或者类簇,关心的是最终创建的是什么,不关心整个的创建过程,只关心结果。
建造者模式的最终目的虽然也是创建对象,但是他同时也参与了创建的过程,对创建的具体细节也参与了,创建的对象更加复杂,通常用于创建一个复合对象。
// 创建一位人"类"
var Human = function (param) {
// 技能
this.skill = param && param.skill || '保密';
// 兴趣爱好
this.hobby = param && param.hobby || '保密';
}
// "类"人原型方法
Human.prototype = {
getSkill: function () {
return this.skill;
},
getHobby: function () {
return this.hobby;
}
}
// 实例化姓名类
var Named = function (name) {
var that = this;
// 构造器
// 构造器函数解析姓名中的姓和名
(function (name, that) {
that.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.workDescript = '每天沉醉于编程';
break;
case 'UI':
case 'UE':
that.work = '设计师';
that.workDescript = '设计更似一种艺术';
break;
case 'teach':
that.work = '教师';
that.workDescript = '分享也是一种快乐';
break;
default:
that.work = work;
that.workDescript = '对不起,我们还不清楚您所选择职位的相关描述';
break;
}
})(work, that);
}
// 更换期望的职位
Work.prototype.changeWork = function (work) {
this.work = work;
}
// 添加对职位的描述
Work.prototype.changeDescript = function (sentence) {
this.workDescript = sentence;
}
建造一个新的类:
/****
* 应聘者建造者
* 参数 name: 姓名(全名)
* 参数work: 期望职位
**/
var Person = function (name, work) {
// 创建应聘者缓存对象
var _person = new Human();
// 创建应聘者姓名解析对象
_person.name = new Named(name);
// 创建应聘者期望职位
_person.work= new Work(work);
// 将创建应聘者对象返回
return _person;
}
继承&实例化
var person = new Person ('xiao ming', code);
从上面的例子中我们可以看到,我们可以通过创建一个局部的对象,然后将其合成一个整体的对象。
提示:
在这种创建模式中我们通常将创建对象的类模块化,这样可以使创建的类的每个模块都可以得到灵活和高质量的运用。
缺点:
这种方式对于整体对象类的拆分无形中增加了结构的复杂性,因此,如果对象粒度很小,或模块间得到复用率很低且变动不大,最好还是创建对象整体。
原型模式
将原型对象指向创建对象的类,使这些类共享原型对象的方法和属性。这种继承是基于对属性和方法的共享,而不是对属性和方法的复制。
下面是一个轮播图插件,用原型模式编写的例子:
var LoopImages = function (imgArr, container) {
this.imagesArray = imgArr; // 轮播图片数组
this.container = container; // 轮播图片容器
this.createImage = function () {} // 创建轮播图片
this.changeImage = function () {} // 切换下一张图片
}
继承轮播图函数
// 上下滑动切换
var SlideLoopImg = function (imgArr, container) {
// 构造函数继承轮播图片类
LoopImgages.call(this, imgArr, container);
// 重写继承的切换下一张图方法
this.changeImage = function () {}
}
// 渐隐切换类
var FadeLoopImg = function (imgArr, container, arrow) {
LoopImages.call(this, imgArr, container);
// 切换箭头私有变量
this.arrow = arrow;
this.changeImage = function () {}
}
具体情况下的使用,如实例化一个渐隐的切换图片类
var fadeImg = new FadeLoopImg([
'01.jpg',
'02.jpg',
'03.jpg',
'04.jpg'
], 'slide', [
'left.jpg',
'right.jpg'
]);
FadeImg.changeImage(); // FadeLoopImg changeImage function
缺点:
在上面说的例子中我们看到基类LoopImages,作为基类是要被子类继承的,那么此时将属性和方法都写在基类的构造函数中,可能存在当父类的构造函数中创建时存在很多耗时较长的逻辑,或每次初始化都要做一些重复性的东西,导致消耗很大,而每次子类继承都要创建一次父类,会导致性能较低。
在这里,为了解决上面的问题,我们可以将这些重复的部分放在原型链上:
这样当我们创建基类时,对于每次创建的一些简单而又差异化的内容我们放在构造函数中,而对一些消耗资源比较大的方法放在基类的原型中,避免很多不必要的消耗,这也是原型模式的一个雏形。
// 图片轮播类
var LoopImage = function (imgArr, container) {
this.imageArray = imgArr; // 轮播图片数组
this.container = container; // 轮播图片容器
}
LoopImages.prototype = {
// 创建轮播图片
createImage: function () {},
changeImage: function () {}
}
继承图片轮播类函数
// 上下滑动切换类
var SlideLoopImg = function (imgArr, container) {
// 构造函数继承图片轮播类
LoopImage.call(this, imgArr, container);
}
SlideLoopImg.prototype = new LoopImage();
// 重写继承下一张图片的方法
SlideLoopImg.prototype.changeImage = function () {}
// 渐隐切换类
var FadeLoopImg = function (imgArr, conyainer, arrow) {
// 切换箭头私有变量
this.arrow = arrow;
}
FadeLoopImg.prototype = new LoopImage();
FadeLoopImg.prototype.changeImage = function () {}
这里我们需要注意的一点是,原型对象是一个共享的对象,那么不论是父类的实例对象还是子类的继承,都是对它的一个指向引用,原型式被共享的,即对原型对象的拓展,不论是子类还是父类的实例对象都会被继承下来的。
特点:
在任何时候都可以对基类或子类进行方法的拓展,而且所有被实例化的对象或者类都能获取这些方法,因此我们有对功能拓展的自由性,而且在修改原型上的方法时容易影响其他使用该方法的子类,产生副作用。
因此原型模式更多的运用在对对象的创建上,比如创建一个实例对象的构造函数比较复杂,或者耗时较长,或者通过创建多个对象来实现,此时最好不要用new关键字来复制基类,但是可以通过对这些对象属性或者方法进行复制来创建。因此原型模式具体方法可以参考下面:
/******
* 基于已经存在的模板对象克隆出新对象的模式
* arguments[0], arguments[1],arguments[2]:参数1,参数2,参数3表示模板对象
* 注意:这里对模板引用类型的属性实质上进行了浅复制(引用类型属性共享),当然根据需求可以自行进行深复制
******/
function prototypeExtend () {
var F = function () {}, // 缓存类,为实例化返回对象临时创建
args = arguments,
i = 0,
len = args.length;
for (; i<len; i++) {
// 遍历每个模版对象中的属性
for (var j in args[i]) {
// 将这些属性复制到缓存类原型中
F.prototype[j] = args[i][j];
}
}
// 返回缓存类实例
var new F();
}
当我们需要让继承对象有独立的原型对象的时候,我们需要对原型对象进行复制,因此,原型对象更适合创建复杂对象,而对于那些需求不停变化而导致对象结构不停改变的情况,将那些比较稳定的属性和方法共用而提取继承的实现。
单例模式
只允许实例化一次的对象类,优势我们也用一个对象来规划一个命名空间,井井有条的管理对象上的属性和方法。
单例模式中为我们提供了一个命名空间即namespace,它用于解决这类问题:为了让代码更易懂,人们常常用单词或拼音定义变量或方法,但是由于人们可用的单词或汉字拼音有限,所以不同的人定义的变量使用的单词名称很可能重复,此时我们可以这样命名:小张写的代码可以定义为一个xiaozhang的命名空间,小张定义的变量可以通过xiaozhang.xx来使用。
另外,我们常常通过单例模式来管理代码库的各个模块。如,jQuery中jQuery.animate()。
具体可以参考下面代码
var Ming = {
g: function (id) {
return document.getElementById(id)
},
css: function (id, key, value) {
// 通过当前对象this来使用g方法
this.g(id).style[key] = value;
}
}
最后,单例模式还可以帮助我们管理静态变量。在JavaScript中没有static关键字,所以任何变量理论上都是可以修改的,所以在JavaScript中实现静态变量很有必要。
静态变量的特点:只能访问不能修改;创建后就能立即使用
在javaScript中我们创建静态变量的思路是,为了避免被修改,我们需要将其放入函数作用域,通过特权方法来访问。为了实现其创建后可以立即使用,我们需要让创建的函数执行执行一次,此时我们创建的对象内保存静态变量通过取值器访问,最后将这个对象作为一个单例放在全局空间中作为静态变量单例对象供他人使用。
var Conf = (function () {
// 私有变量
var conf = {
MAX_NUM: 100,
MIN_NUM: 1,
COUNT: 1000
}
// 返回取值器对象
return {
// 取值器方法
get: function (name) {
return conf[name] ? conf[name] : null;
}
}
})();
// 使用方法
var count = Conf.get('COUNT');
懒惰创建:有些时候对于单例对象需要延迟创建,我们采用延迟创建的形式
// 惰性载入单例
var LazySingle = (function () {
// 单例引用实例
var instance = null;
// 单例
function Single () {
// 这里定义私有属性和方法
return {
publicMethod: function () {},
publicProperty: '1.0'
}
}
// 获取单例对象接口
return function () {
// 如果未创建单例将创建单例
if (!_instance) {
_instance = Single();
}
// 返回单例
return _instance
}
})()
LazySingle().publicProperty
参考文献
张容铭 《JavaScript 设计模式》
网友评论