javascript 常见设计模式
一、单体模式
单体模式是最常见的设计模式,总共有四种:1、简单单体,2、闭包单体, 3、惰性单体, 4、分支单体。
1、简单单体
var singleton = {
attr1: true,
attr2: 10,
method1: function () {},
method2: function () {}
}
补充: 利用简单单体我们可以创建自己的命名空间,这在大型项目中一般会用到,如下:
var TRHL = {}; // 我自己的命名空间
TRHL.singleton = {
attr1: true,
attr2: 10,
method1: function () {},
method2: function () {}
}
2、闭包单体
闭包单体比简单单体安全性更好,有自己的作用域,并且可以在内部定义自己的私有变量
var TRHL = {};
TRHL.singleton = (function () {
var _a1= true;
var _a2 = 10;
var _f1 = function () {alert('f1')};
var _f2 = function () {alert('f2')};
return {
attr1: _a1,
attr2: _a2,
method1: function () {return _f1()},
method2: function () {return _f2()},
}
})()
3、惰性单体
闭包单体有一个问题,就是每次调用都会加载所有属性,比较耗费资源,惰性单体可以解决这个问题:
var TRHL = {};
TRHL.singleton = (function () {
var _uniqInstance; // 定义一个变量,初始为undefined,用于控制是否加载
var _init = function () {
var _a1 = true;
var _a2 = 10;
var _fn1 = function () { alert('fn1') };
var _fn2 = function () { alert('fn2') };
return {
attr1: _a1,
attr2: _a2,
method1: function () { _fn1() };
method2: function () { _fn2() };
}
}
return {
getInstance: function () {
if (!_uniqInstance) {
_uniqInstance = _init();
}
return _uniqInstance;
}
}
})()
TRHL.singleton.getInstance().method1()
惰性单体是底层框架比较常用的设计模式
4、分支单体
分支单体是根据条件来决定加载哪一块属性,一般用来判断浏览器类型
var TRHL = {};
TRHL.singleton = (function () {
var def = true;
var obj1 = {
attr1: '火狐浏览器'
}
var obj2 = {
attr2: 'IE浏览器'
}
return {
def ? obj1 : obj2;
}
})()
二、函数链式调用
1、简单的链式调用
function Dog () {
this.eat = function () {
alert('dog is eating');
return this; // 返回当前调用对象
}
this.run = function () {
alert('dog is running');
return this; // 返回当前调用对象
}
this.sleep = function () {
alert('dog is sleeping');
return this; // 返回当前调用对象
}
}
var d1 = new Dog();
d1.eat().run().sleep();
2、简单模拟jquery底层链式调用
(function (window, undefined) {
// 创建私有类_$
function _$ (arguments) {
// 简单实现一个id选择器
var idSelector;
this.dom;
if (idSelector.test(arguments[0])) {
this.dom = document.getElementById(arguments[0].substring(1));
} else {
throw new Error('arguments is error')
}
};
// 在Function类中拓展一个可以实现链式编程的方法
Function.prototype.method = function (methodName, fn) {
this.prototype[methodName] = fn
}
window.$ = _$; // 将私有类的地址复制给window.$
// 添加方法
_$.prototype.addEvent (type, fn) {
this.dom.addEventListener(type, fn);
return this; // 链式调用
};
_$.prototype.css (prop, val) {
this.dom.style[prop] = val;
return this;
};
_$.onReady = function (fn) {
// 真正在内存中为window.$开辟一个对象空间
window.$ = function () {
return new _$;
}
// 执行传进来的函数
fn();
// 实现链式编程
_$.method('addEvent', function () {}).method('css', function () {});
}
})(window)
$.onReady(function () {
$('#input')
.addEvent('click', function () {
alert('点击我');
})
.css('backgroundColor', 'red');
})
三、工厂模式
工厂模式的核心就是生产对象、实现解耦,使我们的程序更加面向对象、多态化
1、简单工厂
写代码前的思路:我要建立一个卖汽车的商店,它可以出售各种型号的汽车。好了,这只是一个粗略的想法,下一步,我们要让我们的程序细粒度化一些,想要卖汽车,首先要有生产汽车的工厂,它可以生产各种型号的汽车。好,一个简单的工厂模式,就先到此为止,它的思路是:
1、我要有各种型号汽车的类,他们继承自同一种汽车的基础类,都拥有发动和跑的功能。
2、我要有生产各种型号汽车的工厂简单单体,给它一个型号,它就能生产出来相应型号的汽车,并且有发动和跑的功能。
3、最后,我要有卖汽车的商店,它可以卖各种型号的汽车,具有发动和跑的功能。
好的,思路确定了,我们现在需要一些工具,一个是继承的工具,一个是接口工具(检查汽车是否有发动和跑的功能)。我们分别采用寄生继承和鸭式辩型接口作为基础js文件:
// 我自己的命名空间
var TRHL = {};
// 鸭式辩型接口
TRHL.Interface = function (name, methods) {
if (arguments.length !== 2) {
throw new Error('this instance Interface constructor length must be 2 !');
}
this.name = name;
this.methods = [];
for (var i = 1; i < arguments.length; i++) {
if (typeof arguments[i] !== 'string') {
throw new Error('the interface method type is wrong !');
}
}
};
TRHL.Interface.ensureImplements = function (object) {
if (arguments.length < 2) {
throw new Error('Interface.ensureImplements method constructor arguments must be >= 2!');
}
for (var i = 1; i < arguments.length; i++) {
var instanceInterface = arguments[i];
if (instanceInterface.constructor !== TRHL.Interface) {
throw new Error('the arguments constructor not be Interface Class');
}
for (var j = 0; j < instanceInterface.method.length; j++) {
var methodName = instanceInterface.method[j];
if (!object[methodName] || object[methodName] !== 'function') {
throw new Error("the method\'s name '" + methodName + "' is not found!'");
}
}
}
}
// 只继承父类的原型对象
TRHL.extend = function (sub, sup) {
var F = new Function();
F.prototype = new sup();
sub.prototype = F.prototype;
sub.prototype.constructor = sub;
sub.superClass = sup.prototype;
if (sup.prototype.constructor === Object.prototype.constructor) {
sup.constructor = sup;
}
}
好了,工具有了,下面我们开始构建工厂
// 1 各种型号的汽车类,继承自同一个汽车基础类,拥有发动和跑的方法
var carInterface = new TRHL.Interface('carInterface', ['rstart', 'run']); // 建立接口对象
// 基础汽车类
function BaseCar () {};
BaseCar.prototype = {
constructor: BaseCar,
start: function () { alert(this.constructor.name + 'start...') };
run: function () { alert(this.constructor.name + 'run...') };
}
// 各种型号的汽车类
function Benz () {};
TRHL.extend(Benz, BaseCar);
Benz.prototype.drive = function () { alert('Benz drive') }; // 可以添加自己的方法
function BMW () {};
TRHL.extend(BMW, BaseCar);
function Audi () {};
TRHL.extend(Audi, BaseCar);
// 2 生产汽车的工厂简单单体,给它一个型号,它就能生产出来相应型号的汽车,并且有发动和跑的功能。
carFactory = {
createCar: function (type) {
var car = null;
switch (type) {
case 'Benz': car = new Benz();
case 'BMW': car = new BMW();
case 'Audi': car = new Audi();
default: alert('no this type car !');
}
TRHL.Interface.ensureImplements(car, carInterface); // 用于检查汽车实例是否具有发动和跑的方法
return car;
}
}
// 3 卖汽车的商店类,可以出售各种型号的汽车,并且具有发动和跑的功能
function CarShop () {};
CarShop.prototype = {
constructor: CarShop,
sellCar: function (type) {
var car = carFactory.createCar(type);
return car;
}
}
// 使用方法:
var shop1 = new CarShop();
c1 = shop1.sellCar('Benz');
c1.start();
c1.run();
c1.drive();
好的简单工厂就这样构建好了,这就是面向对象解决问题的方法,思路是最重要的。
2、复杂工厂
那么,在简单工厂的基础上,我们可以继续的细化,首先,卖汽车的商店不可能什么样的汽车都卖,所以要有不同的商店,出售不同品牌的汽车,其次,生产汽车的工厂代码结构可以继续优化一下,对于第一个问题,可以引入抽象类的概念,将商店转化为抽象类,第二个问题,可以用eval来解决:
// 1 将商店转化为抽象类
function CarShop () {};
CarShop.prototype = {
constructor: CarShop,
sellCar: function (type) {
this.abstractSellCar(type);
},
abstractSellCar: function () {
throw new Error('this method is abstract...');
}
}
// 2 建立出售各种品牌汽车的商店类
function BenzCarShop () {};
TRHL.extend(BenzCarShop, CarShop);
BenzCarShop.prototype = {
constructor: BenzCarShop,
sellCar: function (type) { // 覆写sellCar方法
var car = null;
var types = ['Benz']; //有各种型号的Benz汽车
for (t in types) {
if (types[t] === type) {
car = carFactory.createCar(type);
return car;
} else {
throw new Error('no this type car !');
}
}
}
}
function BMWCarShop () {};
TRHL.extend(BMWCarShop, CarShop);
BMWCarShop.prototype = {
constructor: BMWCarShop,
sellCar: function (type) { // 覆写sellCar方法
var car = null;
var types = ['BMW']; //有各种型号的BMW汽车
for (t in types) {
if (types[t] === type) {
car = carFactory.createCar(type);
return car;
} else {
throw new Error('no this type car !');
}
}
}
}
function AudiCarShop () {};
TRHL.extend(AudiCarShop, CarShop);
AudiCarShop.prototype = {
constructor: AudiCarShop,
sellCar: function (type) { // 覆写sellCar方法
var car = null;
var types = ['Audi']; //有各种型号的Audi汽车
for (t in types) {
if (types[t] === type) {
car = carFactory.createCar(type);
return car;
} else {
throw new Error('no this type car !');
}
}
}
}
// 优化生产汽车的工厂
var carFactory = {
createCar: function (type) {
var car = null;
car = eval('new '+type+'()');
TRHL.Interface.ensureImplements(car, CarInterface)
return car;
}
}
// 各种型号的汽车类不变
var CarInterface = new TRHL.Interface('CarInterface', ['start', 'run']);
function BaseCar () {};
BaseCar.prototype = {
constructor: BaseCar,
start: function () { alert(this.constructor.name + '...start') },
run: function () { alert(this.constructor.name + '...run') },
}
function Benz () {};
TRHL.extend(Benz, BaseCar);
Benz.prototype.BenzDrive = function () { alert('Benz drive') };
function BMW () {};
TRHL.extend(BMW, BaseCar);
function Audi () {};
TRHL.extend(Audi, BaseCar);
// 使用方法:
var shop1 = new BenzCarShop();
c1 = shop1.sellCar('Benz');
c1.run();
c1.drive();
var shop2 = new AudiCarShop();
c2 = shop2.sellCar('Audi');
c2.run();
四、桥模式
桥模式的目的主要是为了把两个业务逻辑单元解耦,方便单元测试,主要有三种应用场景:1、事件监听回调,2、特权函数,3、组合单体
1、事件监听回调
// 注册点击事件业务逻辑
var inp = document.getElementById('inp');
inp.addEventListener('click', bridgeHandler, false);
// 桥模式解耦
function bridgeHandler (this.value) {
var msg = this.value;
sendRequest(msg);
}
// 发送数据业务逻辑
function sendRequest (msg) {
alert('成功发送数据'+msg);
}
2、特权函数
function PublicClass () {
// 内部私有函数
function privateMethod () {
alert('执行了很复杂的操作')
}
// 通过桥特权函数调用私有函数
this.bridgeMethod = function () {
privateMethod();
}
}
var p1 = new PublicClass();
p1.bridgeMethod(); // 调用桥特权函数
3、组合单体
function Class1 (a, b, c) {
this.a = a;
this.b = b;
this.c = c;
}
function Class2 (d, e) {
this.d = d;
this.e = e;
}
function BridgeClass (a, b, c, d, e) {
this.class1 = new Class1(a, b, c);
this.class2 = new Class2(d, e);
}
桥模式的代码并不复杂,重要的是理解思想:解耦业务逻辑,方便单元测试。
五、组合模式
组合模式通常用来处理树状结构,组合模式的应用场景和特点:
场景:
1 存在一批组织成某种层次体系的对象
2 希望对这批对象或其中的一部分对象执行一个操作
特点:
1 组合模式中只有两种类型的对象:组合对象、叶子对象
2 这两种类型都实现同一批接口
3 一般我们会在组合对象中调用其方法并隐式调用“下级对象”的方法(一般采用递归的形式去做)
举个例子:假如我们现在有如下一个树状结构:
公司 ---->财务部门----->张一
----->张二
----->张三
----->销售部门----->张四
----->张五
----->张六
具体的方法只能由树状结构的末端每一个人去执行,那么我们如何去构建这个树状结构?传统的方法可能是这样的:
// 总公司类
function Org (name) {
this.name = name;
this.depts = [] //保存下级部门
}
Org.prototype = {
constructor: Org,
addDepts: function (child) { this.depts.push(child); return this; };
getDepts: function () { return this.depts; }
}
// 部门类
function Dept (name) {
this.name = name;
this.persons = [] //保存下级员工
}
Dept.prototype = {
constructor: Dept,
addPersons: function (child) { this.persons.push(child); return this; };
getPersons: function () { return this.persons; }
}
// 员工类
function Person (name) {
this.name = name;
}
Person.prototype = {
constructor: Person;
hardWorking: function () { document.write(this.name + 'hard working...') };
sleeping: function () { document.write(this.name + 'sleeping...') };
}
// 生产对象
var p1 = new Person('张1');
var p2 = new Person('张2');
var p3 = new Person('张3');
var p4 = new Person('张4');
var p5 = new Person('张5');
var p6 = new Person('张6');
var dept1 = new Dept('财务部门');
dept1.addPersons(p1).addPersons(p2).addPersons(p3);
var dept2 = new Dept('销售部门');
dept2.addPersons(p4).addPersons(p5).addPersons(p6);
var Org = new Org('总公司');
Org.addDepts(dept1).addDepts(dept2);
// 需求:具体的让张3工作
// 这个时候,我们要通过一层层的循环将张3找到,调用hardWorking方法
for (var i = 0, dept = Org.getDepts(); i < dept.length; i++) {
for (var j = 0; person = dept[i].getPersons(); j < person.length; j++) {
if (person[j] === '张3') {
person[j].hardWorking();
}
}
}
那么现在问题来了,如果公司的组织结构发生了变化怎么办?或者需求发生改变:我想让某个部门的人员努力工作怎么办?这对于传统方法,是非常麻烦的,需要重写整个结构和循环。这个时候,我们就会用到组合模式,让我们再来看一下组合模式的应用场景和特点:
场景:
1 存在一批组织成某种层次体系的对象
2 希望对这批对象或其中的一部分对象执行一个操作
特点:
1 组合模式中只有两种类型的对象:组合对象、叶子对象
2 这两种类型都实现同一批接口
3 一般我们会在组合对象中调用其方法并隐式调用“下级对象”的方法(一般采用递归的形式去做)
我们希望只通过Org.hardWorking(‘XXX’),就可以让我想要的对象执行hardWorking,好,我们让树状结构再复杂一点,然后按照组合模式的特点对树状结构进行代码重构:
公司 -----北京分公司---->北京财务部门----->张一
---->张二
----->张三
- ---->北京销售部门----->张四
----->张五
----->张六
-----上海分公司---->上海财务部门----->张七
----->张八
----->张九
----->上海销售部门----->张十
----->张十一
----->张十二
组合对象的本质其实还是递归循环,所以我们先写一个方法,目的是可以循环多维数组,并让每个元素执行一个回调函数fn:
Array.prototype.each = function (fn) {
try {
this.i || (this.i = 0); // 计数器,记录当前遍历元素的位置
if (this.length > 0 && fn.constructor == Function) {
// 循环遍历数组
while(this.i < this.length) {
// 获取数组的每一项
var el = this[this.i];
// 如果当前元素获取到了,并且当前元素是一个数组
if (el && el.constructor == Array) {
// 递归操作
el.each(fn);
} else {
fn.call(el, el); // 将fn绑定在el上,并且将el作为参数传过去
}
this.i++;
}
this.i = null; //释放内存
}
} catch (error) {
// do something
}
return this;
}
好了,下面正式开始:
// 组合对象的接口和叶子对象的接口,接口是用鸭式辩型法写的
var CompositeInterface = TRHL.Interface('CompositeInterface', ['addChild', 'getChild']);
var LeafInterfece = TRHL.Interface('LeafInterfece', ['hardWorking', 'sleeping']);
// 组合对象的类
function Composite (name) {
this.name = name;
this.type = 'Composite'; // 表明类型是组合对象
this.children = [];
}
// 让组合对象实现统一的接口'addChild', 'getChild','hardWorking', 'sleeping'
Composite.prototype = {
constructor: Composite,
addChild: function (child) {
this.children.push(child);
return this; // 链式调用
},
getChild: function (name) { // 组合对象的查找方法,用来递归查找某个组合对象下面的叶子对象,因为叶子对象是具体执行者
var element = []; // 装载叶子对象的数组
// 定义一个函数,递归将叶子对象推入element
var pushLeaf = funciton (item) {
if (item.type === 'Composite') {
item.children.each(arguments.callee); // 如果是组合对象,递归调用pushLeaf
} else if (item.type === 'Leaf') {
element.push(item); // 如果是叶子对象,推入element
}
}
if (name && this.name !== name) { // 传入name的情况,需要按name递归查找
this.children.each(function (item) {
// 1 如果找到匹配的name,递归推入element
if (item.name === name && item.type === 'Composite') {
item.children.each(pushLeaf);
}
// 2 如果没找到匹配的name,递归调用当前函数,继续循环
if (item.name !== name && item.type === 'Composite') {
item.children.each(arguments.callee)
}
// 3 如果传入的name是叶子对象,直接推入element
if (item.name === name && item.type === 'Leaf') {
element.push(item);
}
})
} else { // 没有传入name的情况,递归查找所有叶子对象
this.children.each(pushLeaf);
}
return element;
},
hardWorking: function () {
var leafObject = this.getChild();
for (var i = 0; i < leafObject.length; i++) {
leafObject[i].hardWorking();
}
},
sleeping: function () {
var leafObject = this.getChild();
for (var i = 0; i < leafObject.length; i++) {
leafObject[i].sleeping();
}
}
}
// 叶子对象的类
function Leaf (name) {
this.name = name;
this.type = 'Leaf'; // 表明类型是叶子对象
}
// 让叶子对象实现统一的接口'addChild', 'getChild','hardWorking', 'sleeping'
Leaf.prototype = {
constructor: Leaf,
addChild: function () {
throw new Error('this method is disabled...') // 叶子节点无法添加子节点
},
getChild: function (name) {
if (this.name === name) {
return this;
}
return null
},
hardWorking: function () {
document.write(this.name + 'hardWorking');
},
sleeping: function () {
document.write(this.name + 'sleeping');
}
}
// 建立对象,传入数据
var p1 = new Leaf('张1');
var p2 = new Leaf('张2');
var p3 = new Leaf('张3');
var p4 = new Leaf('张4');
var p5 = new Leaf('张5');
var p6 = new Leaf('张6');
var p7 = new Leaf('张7');
var p8 = new Leaf('张8');
var p9 = new Leaf('张9');
var p10 = new Leaf('张10');
var p11 = new Leaf('张11');
var p12 = new Leaf('张12');
var dept1 = new Composite('北京财务部门');
dept1.addChild(p1).addChild(p2).addChild(p3);
var dept2 = new Composite('北京销售部门');
dept2.addChild(p4).addChild(p5).addChild(p6);
var dept3 = new Composite('上海财务部门');
dept3.addChild(p7).addChild(p8).addChild(p9);
var dept4 = new Composite('上海销售部门');
dept4.addChild(p10).addChild(p11).addChild(p12);
var Org1 = new Composite('北京分公司');
Org1.addChild(dept1).addChild(dept2);
var Org2 = new Composite('上海分公司');
Org2.addChild(dept3).addChild(dept4);
var Org = new Composite('总公司');
Org.addChild(Org1).addChild(Org2);
Org.hardWorking();
Org.hardWorking('北京分公司');
Org.hardWorking('上海财务部门');
Org.hardWorking('张10');
好了,这就是组合模式,其实就是将树结构拆分成了组合对象和叶子对象,并且把循环递归提炼了出来,这样,不管需求如何变化,结构怎么改变,都非常方便。
六、门面模式
门面模式,是所有库的基础,也是程序员最常用的模式,就是用一个公用的方法把一些复杂的操作和函数提炼封装起来,达到复用和解耦的目的。
function a(x) { alert(x) };
function b(y) { alert(y) };
// 用门面模式封装起来
function ab (x, y) {
a(x);
b(y);
}
这就是最简单的门面模式,有点类似于桥模式的第三种用法,组合多个对象,当然,门面模式不止如此,它还可以更复杂一些,
现在我有一个元素,想要给它设置颜色样式:
window.onload(function () {
var el = document.getElementById('div1');
el.style.color = 'red';
})
现在需求变了,我有多个元素,想要给它们设置某个样式,这时候为了避免重复性的代码,就可以使用门面模式:
// 用门面模式封装
function setStyle (el, prop, val) {
for (var i = 0; i < el.length; i++) {
document.getElementById(el[i]).style[prop] = val;
}
}
setStyle(['div1', 'div2', 'div3'], 'color', 'blue');
现在需求又变了,我有多个元素,并且我想给它们设置多个样式,这个时候我们就可以用门面模式进行二次封装:
function setCss (el, options) {
for (prop in options) {
setStyle(el, prop, options[prop]);
}
}
setCss (['div1', 'div2', 'div3'], {
color: 'red',
fontSize: '20px'
})
这就是门面模式,但是要注意不要滥用门面模式,否则就会把简单的问题复杂化
七、适配器模式
适配器模式可以处理一些类与API不匹配、不能一同使用的情况,使用这种模式的对象又叫做“包装器”(wrapper)。注意适配器模式虽然和门面模式比较类似,但是门面模式只是为了简化一个接口,不提供额外的选择;适配器则要把一个接口转化为另一个接口,它并不会滤除某些能力,也不会简化接口。
举个例子:
比如现在要开发一个项目,有些程序员会Prototype的框架,有些程序员会YUI的框架,这时候我们应该选用什么框架?
这个时候就用到适配器了,我们可以选用YUI的框架,但是Prototype的程序员,按照Prototype框架的规则,该怎么写还怎么写。
就拿这两个框架的dom选择器做一个例子,这个是Prototype获取dom元素的方法:
$('div1', 'div2')
可以传一个id,也可以传多个id,它的底层实现大概是这样:
// Prototype 不需要传递任何参数, 一个id写一个参数,多个id写多个参数
function $ () {
var elements = [];
for (var i = 0; i < arguments.length; i++) {
var element = arguments[i];
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (arguments.length === 1) {
return element;
}
elements.push(element);
}
return elements;
}
这是YUI获取dom元素的方法:
YAHOO.get('div1');
YAHOO.get(['div1', 'div2'])
只能传递一个参数,要么是一个字符串,要么是一个数组,它的底层实现大概是这样的:
// YUI 传递了一个参数, 类型是数组或者字符串
var YAHOO = {};
YAHOO.get = function (el) {
if (typeof el === 'string') {
return document.getElementById(el);
}
if (el instanceof Array) {
var elements = [];
for (var i = 0; i < el.length; i++) {
elements[elements.length] = YAHOO.get(el[i]);
}
return elements;
}
if (el) {
return el;
}
return null;
}
下面,开始做一个适配器,目的是让Prototype程序员可以使用YAHHO的框架,但是用Prototype的规则写代码:
YAHOO.get = YUIToPrototypeAdapter;
function YUIToPrototypeAdapter () {
if (arguments.length === 1) { // 这是YUI常用的写法,只传递一个参数
var e = arguments[0];
return $.aplly(window, e instanceof Array ? e : [e]);
} else { // 这是Prototype程序员的写法,传递了多个id
return $.apply(window, arguments);
}
}
好了,使用了这个适配器之后,prototype的程序员就可以这样写代码了:
YAHOO.get('div1', 'div2');
雅虎的程序员也可以像原来一样写:
YAHOO.get(['div1', 'div2']);
八、装饰者模式
装饰者模式是一种为对象添加新特性的技术,它可以透明的把对象包装在具有相同接口的另一个对象中。这样我们就可以为对象添加一些方法或者行为,然后将方法调用传递给原始对象,我们先来看看再函数中它是如何使用的,比如我现在有下面这样一个函数:
function getDate () {
return (new Date()).toString();
}
现在,我想要让这个函数的返回的结果全部大写,这个时候做一个装饰器就好:
function upperCaseDecorator (fn) {
return function () {
return fn.apply(this, arguments).toUpperCase();
}
}
// 然后这样调用就可以
var getDecoratorDate = upperCaseDecorator(getDate);
alert(getDecoratorDate());
可以看到,我们在不改变原有函数的基础上,为函数添加了新的特性,下面看一下装饰者模式在面向对象中的使用,假如我现在有一个汽车的类如下:
function Car (car) {
// 这个属性,就是为了让子类继承的,可以让子类多一个父类的引用
// 这样子类在继承了这个属性之后,将来构造子类的实例时可以传入父类的实例,存在这里,方便子类的实例进行引用,装饰,修改
this.car = car;
}
Car.prototype = {
constructor: Car,
getPrice: function () { return 100000 },
assemble: function () { document.write('组装汽车') }
}
var car = new Car();
现在,我想在不改变原有类的基础上,改写一下里面的方法,同时,在我新的方法中,还要引用原来的类中的方法,比如,我创建一个给汽车安装车灯的类,安装车灯之后,汽车的价格会在原有100000元的基础上加1000,这个时候就需要装饰者模式:
function LightDecorator (car) {
// 这句话继承了Car类的构造函数,为自己添加了一个this.car = car;
LightDecorator.superClass.constructor.call(this, car);
}
// 这句话继承了Car类的原型
TRHL.extend(LightDecorator, Car);
LightDecorator.prototype = {
constructor: LightDecorator,
getPrice: function () { return this.car.getPrice() + 10000 }, // this.car引用了原始Car类里面的getPrice方法
assemble: function () { document.write('组装车灯...') }
}
// 使用方法
var car = new Car();
car = new LightDecorator(car); // 注意这里传入了原始类的实例,存在了this.car中
alert(car.getPrice()); // 110000
补充一下extend方法:
// 实现只继承父类的原型对象
TRHL.extend = function (sub, sup) {
var F = new Function();
F.prototype = sup.prototype;
sub.prototype = new F();
sub.prototype.constructor = sub;
sub.superClass = sup.prototype;
if (sup.prototype.constructor === Object.prototype.constructor) {
sup.constructor = sup;
}
}
这就是装饰者模式,顾名思义,其是对原始对象的装饰,所以必然是要引用原始对象的。
九、享元模式
享元模式是一种优化模式,它最适合解决因创建大量类似对象而累及性能的问题。这种模式在javascript中尤其有用,因为复杂的javascript代码可能很快会用光浏览器的所有内存。通过把大量独立对象转化为少量共享对象,可以降低运行web应用程序所需的资源数量。下面通过2个例子来说明:
1、划分内在数据和外在数据
当一个对象内部有很多属性,我们在创建大量这种对象时,可以将对象的属性划分为内在数据(static)和外在数据,内在数据是可能会大量重复的数据,外在数据是很可能各不相同,比如我现在有一个传统的汽车的类:
function Car (make, model, year, owner, tag, renewDate) { // 属性分别是: 出场商 型号 生产日期 拥有者 车牌号 最近登记时间
this.make = make;
this.model = model;
this.year = year;
this.owner = owner;
this.tag = tag;
this.renewDate = renewDate;
}
Car.prototype = {
constructor: Car,
getMake: function () { return this.make; },
getModel: function () { return this.model; },
getYear: function () { return this.year; },
renewRegistration: function (newRnewDate) {
this.renewDate = newRnewDate;
}
}
好,现在我要创建500万个汽车实例,让我们测试一下在谷歌浏览器,它会耗费多少资源:
var stime = new Date();
var arr = [];
for (var i = 0; i < 5000000; i++) {
arr.push(new Car('上海大众', '迈腾', '2019-10-11', 'TRHL', 'bj0001', '2019-10-11'));
}
var etime = new Date();
console.log(etime - stime); // 占用内存652M 运行时间1613ms
现在,让我们用享元模式来优化一下,首先分析一下Car类的属性,我们发现(出场商,型号,生产日期)可能很多都是一样的,我们将其划分为内在数据;(拥有者,车牌号,登记时间)这个很可能各不相同,我们将其划分为外在数据。下面我们的思路是:能不能将内在数据的重复项去掉,然后和外在数据组合起来,这样就可以节省创建相同内在数据所耗费的资源,好了,思路确定了,怎么实施?首先创建2个闭包单体,一个用来生产内在数据,一个用来整合,当然原始的Car类也需要改造一下:
function Car (make, model, year) { // 将外在数据和方法都去掉
this.make = make;
this.model = model;
this.year = year;
}
Car.prototype = {
constructor: Car,
getMake: function () { return this.make; },
getModel: function () { return this.model; },
getYear: function () { return this.year; }
}
// 第一个闭包单体,作为生产内在数据的工厂
var CarFactory = (function () {
var createdCars = {}; // 用于承载创建的内在数据实例
return {
createCar: function (make, model, year) {
// 将make+model+year作为唯一的标识key,如果有重复的,返回当前对象,不创建新对象
if (createdCars[make+model+year]) {
return createdCars[make+model+year];
} else {
var car = new Car(make, model, year);
createdCars[make+model+year] = car;
return car;
}
}
}
})()
// 第二个闭包单体,用于整合外在数据和内在数据
var carRecordManager = (function () {
var carRecordDataBase = {}; // 用于承载整合好的实例
return {
addCarRecord: function (make, model, year, owner, tag, renewDate) {
var car = CarFactory.createCar(make, model, year);
carRecordDataBase[tag] = { // 将tag作为唯一标识key
car: car,
owner: owner,
renewDate: renewDate
}
return carRecordDataBase[tag];
}
// 当然还要添加外在数据的方法
renewRegistration: function (tag, newRenewDate) {
carRecordDataBase[tag].renewDate = newRenewDate;
}
}
})()
好了,我们用享元模式改造完了,下面测试一下:
var stime = new Date();
var arr = [];
for (var i = 0; i < 5000000; i++) {
arr.push(CarRecordManager.addCarRecord('上海大众', '迈腾', '2019-10-11', 'TRHL', 'bj0001', '2019-10-11'));
}
var etime = new Date();
console.log(etime - stime); // 占用内存551M 耗费时间397ms
怎么样,是不是优化了很多?享元模式,顾名思义,就是找到重复项,去除重复项,共享同一个方法或者对象。
2、日历优化
日历组件在开发中经常用到,比如我现在要写一个日历组件,我的思路是什么?首先,要有3个类,年类,月类,日类。类之间还有包含的关系,年包含月,月包含日,那么,我希望我new一个年的类,它可以创建12个月的类,每个月的类还要创建30个左右日的类,当然,为了展示日历,每个类还需要一个display的方法,好了,思路确定了,开始撸代码:
// 年类
var CalendarYear = function (year, parent) { // 传入年份和挂载的父节点
this.year = year;
this.element = document.createElement('div');
this.element.style.display = 'none';
parent.appendChild(this.element);
this.months = []; // 包含月
// 判断闰年
var isLeapYear = function (y) {
return (y > 0) && !(y % 4) && ((y % 100) || !(y % 400));
}
this.numDays = [31, isLeapYear(this.year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // 每个月有多少天
for (var i = 0 ; i < 12; i++) {
this.months[i] = new CalendarMonth(i, this.numDays[i], this.element); // new出12个月的实例
}
}
// 添加年的display方法
CalendarYear.prototype = {
constructor: CalendarYear,
display: function () {
for (var i = 0; i < this.months.length; i++) {
this.months[i].display();
}
this.element.style.display = 'block';
}
}
// 月类
var CalendarMonth = function (monthNum, numDays, parent) {
this.monthNum = monthNum;
this.element = document.createElement('div');
this.element.style.display = 'none';
parent.appendChild(this.element);
this.days = [];
for (var i = 0; i < numdays; i++) {
this.days[i] = new CalendarDay(i + 1, this.element); // new相应天数的CalendarDay
}
}
CalendarMonth.prototype = {
constructor: CanlendarMonth,
display: function () {
for (var i = 0; i < numDays; i++) {
this.days[i].display();
}
this.element.style.display = 'block';
}
}
// 日类
var CalendarDay = function (date, parent) {
this.date = date;
this.element = document.createElement('div');
this.element.style.display = 'none';
parent.appendChild(this.element);
}
CalendarDay.prototype = {
constructor: CalendarDay,
display: function () {
this.element.style.display = 'inline-block';
this.element.innerHTML = this.date;
}
}
// 创建日历
window.onload = function () {
var app = document.getElementById('app');
var myCalendar = new CalendarYear('2019', app);
myCalendar.display();
}
好了,日历写完了,我们可以看到,每new一个CalendarYear,就会new出来12个CalendarMonth和365个CalenderDay,可以发现,性能的瓶颈是那365个CalendarDay,可不可以用享元模式优化一下呢,再看一下日类:
// 日类
var CalendarDay = function (date, parent) {
this.date = date;
this.element = document.createElement('div');
this.element.style.display = 'none';
parent.appendChild(this.element);
}
CalendarDay.prototype = {
constructor: CalendarDay,
display: function () {
this.element.style.display = 'inline-block';
this.element.innerHTML = this.date;
}
}
可以看出来,365个日类基本没有什么太大的不同,对它进行如下改造:
var CalendarDay = function () {};
CalendarDay.prototype = {
constructor: CalendarDay,
display: function (date, parent) {
var element = document.createElement('div');
parent.appendChild(element);
element.style.display = 'inline-block';
element.innerHTML = date;
}
}
var calendarDaySingleInstance = new CalendarDay;
现在月类没必要new出来365个对象了,我们只需要calendarDaySingleInstance这一个对象,再给月类的display传参就可以了,修改月类代码如下:
// 月类
var CalendarMonth = function (monthNum, numDays, parent) {
this.monthNum = monthNum;
this.element = document.createElement('div');
this.element.style.display = 'none';
parent.appendChild(this.element);
this.days = [];
for (var i = 0; i < numdays; i++) {
this.days[i] = calendarDaySingleInstance; // 只需要new 一个对象就可以了
}
}
CalendarMonth.prototype = {
constructor: CanlendarMonth,
display: function () {
for (var i = 0; i < numDays; i++) {
this.days[i].display(i + 1, this.element); // 传入参数
}
this.element.style.display = 'block';
}
}
所有的日类共享一个元类:calendarDaySingleInstance,不用拓展新的内存了,日历的功能没有任何影响。
这就是享元模式,关键在于分析重复性的类,数据,从而达到改造,优化的目的。
十、代理模式
代理是一个对象,它可以用来控制对一个对象的访问。它与这个对象实现了同样的接口,并且会把任何方法调用传递到这个接口,代理对象不会添加方法或者修改方法(如装饰者),也不会简化接口(如门面模式)。它实行的接口与本体完全相同,所有对它进行的方法调用都会传递给本体。举个例子,我现在有一个图书馆的类:
// 书籍的类
var Book = function (id, title, author) {
this.id = id;
this.title = title;
this.author = author;
}
// 图书馆的类
var Library = function (books) {
this.books = books;
}
Library.prototype = {
constructor: Library,
addBook: function (book) {
this.books[id] = book;
},
findBook: function (id) {
if (this.books[id]) {
return this.books[id];
}
return null;
},
borrowBook: function (id) {
// 电脑登记,交押金
return this.findBook(id);
},
returnBook: function (book) {
// 电脑登记...已还
// 计算费用
this.books[book.id] = book;
}
}
现在,我不想每一次都实例化图书馆这个类,我想要对它的实例化进行节制,这个时候就需要创建一个代理对象:
var LibraryProxy = function (books) {
alert('产生了代理对象,并没有真正产生本体对象');
this.books = books;
this.library = null;
}
LibraryProxy.prototype = {
constructor: LibraryProxy,
initializeLibrary: function () {
if (this.library === null) {
this.library = new Library(this.books);
}
},
addBook: function (book) {
this.initializeLibrary();
this.library.addBook(book);
},
findBook: function (id) {
this.initializeLibrary();
alert('产生了本体对象');
return this.library.findBook(id);
},
borrowBook: function (id) {
this.initializeLibrary();
this.library.borrowBook(id);
},
returnBook: function (book) {
this.initializeLibrary();
this.library.borrowBook(book);
}
}
好了,代理对象写完了,这样使用:
var proxy = new LibraryProxy({
'01': new Book('01', 'Javascript', 'z3'),
'02': new Book('02', 'Java', 'z4')
})
// 在调用下面这个方法之前,并没有真正实例化图书馆对象
alert(proxy.findBook('02').author);
十一、观察者模式
观察者模式可以对程序中某个对象的状态进行观察,并在其发生改变时能够得到通知,观察者模式中存在两种角色:观察者和被观察者,也可以成为发布者和订阅者,用例子来说明:
// 被观察者的类(发布者)
var Publish = function (name) {
this.name = name;
// 1 被观察者(发布者)的类中要有观察者(订阅者)的集合
// 2 集合里的元素必须是函数类型,因为观察者模式的本质就是状态改变时(发布消息时)触发执行函数,从而让观察者得到通知
this.subscribers = []; // fn1, fn2, .....
}
// 被观察者的状态改变,或者说是发布者发布消息
Publish.prototype.deliver = function (news) {
var publish = this; // 保存一下当前被观察者(发布者)的信息
// 观察者模式的核心,当调用deliver方法时,就会触发自己所有的观察者执行fn函数
this.subscribers.forEach(function (fn) {
fn(news, publish);
})
}
// 观察者开始观察被观察者 或者说是 订阅者开始订阅发布者
Function.prototype.subscribe = function (publish) {
var sub = this; // 保存一下观察者(订阅者)的信息
// 1 判断此观察者是否已经存在
var alreadyExists = publish.subscribers.some(function (item) {
return item === this;
})
// 2 如果不存在,将其加入到被观察者的subscribers中去
if (!alreadyExists) { publish.subscribers.push(this) }
return this; //链式调用
}
// 取消观察(订阅)的方法
Function.prototype.unsubscribe = function (publish) {
var sub = this;
// 将观察者从subscribers中删除
publish.subscribers = publish.subscribers.filter(function (item) {
return item !== this
})
return this;
}
这就是观察者模式的基本架构,具体这样使用:
// 实例化被观察者(发布者)
var pub1 = new Publish('被观察者1');
var pub2 = new Publish('被观察者2');
var pub3 = new Publish('被观察者3');
// 实例化观察者(订阅者)其实每个函数都可以是观察者(订阅者)
var sub1 = function (news) { alert(news, arguments[1]) };
var sub2 = function (news) { alert(news, arguments[1]) };
// 开始观察(订阅消息)
sub1.subscribe(pub1).subscribe(pub2).subscribe(pub3);
sub2.subscribe(pub1).subscribe(pub2);
// 状态改变(发布消息)
pub1.deliver('我的状态改变了'); // sub1 sub2 都会收到
pub3.deliver('我的状态也改变了'); // sub2收不到
// 还可以取消观察(订阅)
sub1.unsubscribe(pub3);
十二、命令模式
命令模式是一种封装方法调用的方式,看到了封装,我们会想到门面模式,但是命令模式与门面模式有所不同:门面模式是对一些重复性、程序化的函数进行封装,方便以后调用;而命令模式可以对方法调用进行参数化处理和传送,也就是说,命令模式更侧重于对某个对象的部分行为进行精确的控制。看到这里,我们是否会想到游戏?没错,游戏中经常使用命令模式。
命令模式的整体架构分为三个部分:1、客户,2、调用者,3、接收者。
客户创建命令,调用者执行命令…只是执行命令,具体的脏活累活谁来干?接收者,他在执行命令时进行相应的操作。
下面用一个例子说明一下命令模式的简单架构:
// 我是接收者,脏活累活我来干
var worker = {
work1: function (a, b) { alert(a, b, '复杂的操作') },
work2: function (c, d) { alert(c, d, '还是复杂的操作') },
work2: function (e, f) { alert(e, f'依然复杂的操作') },
}
// 我是调用者,我记住了很多命令,我只执行各种命令
var Command1 = function (worker) {};
Command1.prototype.execute = function () {
worker.work1(a+10, b-10); // 可以传参
worker.work3(e-10, f+10); // 可以传参
}
var Command2 = function (worker) {};
Command1.prototype.execute = function () {
worker.work2(c+10, d+10); // 可以传参
}
// 我是客户我最牛,我决定用啥命令
var c1 = new Command1(worker);
c1.execute();
理解了命令模式的基本架构之后,再用一个实际的例子加深一下理解,比如我现在做了一个功能,可以通过改变参数,让一个小方块上下左右移动,并且我把它封装成一个类,可以重复生成实例:
var Cursor = function (width, height, parent) { // 画布的宽高,挂载的结点
this.width = width;
this.height = height;
this.position = { // 用于保存小方块的位置
x: width / 2,
y: height / 2
}
// 创建画布
this.canvas = document.createElement('canvas');
this.canvas.width = this.width;
this.canvas.height = this.height;
parent.appendChild(this.canvas);
// canvas上下文元素
this.ctx = this.canvas.getContext('2d');
this.ctx.fillStyle = 'red';
this.move(0, 0);
}
Cursor.prototype = {
constructor: Cursor,
move: function (x, y) {
this.position.x += x;
this.position.y += y;
this.canvas.clearRect(0, 0, this.width, this,height);
this.canvas.fillRect(this.position.x, this.position.y, 20, 20);
}
}
现在,我想把小方块的移动细分为上下左右,四个命令,我每调用一次命令,它就会向一个方向移动,这个时候就用到了命令模式:
// 向上移动的命令
var MoveUp = function (cursor) {
this.cursor = cursor; // 保存一下接收者
}
MoveUp.prototype = {
constructor: MoveUp,
execute: function () {
cursor.move(0, -10);
}
}
// 向下移动的命令
var MoveDown = function (cursor) {
this.cursor = cursor; // 保存一下接收者
}
MoveDown.prototype = {
constructor: MoveDown,
execute: function () {
cursor.move(0, 10);
}
}
// 向左移动的命令
var MoveLeft = function (cursor) {
this.cursor = cursor; // 保存一下接收者
}
MoveLeft.prototype = {
constructor: MoveLeft,
execute: function () {
cursor.move(-10, 0);
}
}
// 向右移动的命令
var MoveRight = function (cursor) {
this.cursor = cursor; // 保存一下接收者
}
MoveRight.prototype = {
constructor: MoveRight,
execute: function () {
cursor.move(10, 0);
}
}
上面就是调用者,下面写一下客户:
var body = document.getElementByTagName('body')[0];
var cursor = new Cursor(400, 400, body);
var up = new MoveUp(cursor);
var down = new MoveDown(cursor);
var left = new MoveLeft(cursor);
var right = new MoveRight(cursor);
// 执行up命令
up.execute();
可以发现,我们通过命令模式,就可以对一个对象进行精确的操控。
十三、责任链模式
责任链模式可以用来消除请求的发送者和接收者之间的耦合。链中的每个对象可以处理请求,也可以将其传给下一个对象。javascript内部就是使用了这个模式来处理时间捕获和冒泡的问题。
责任链模式中的角色:请求发出者, 接收者。
责任链模式的流程:
1、发送者知道链中的第一个接收者,它向这个接收者发出请求。
2、每一个接收者对请求进行分析,要么处理它,要么往下传递。
3、每一个接收者知道的其他对象只有一个,即它的下家对象。
4、如果没有任何接收者处理请求,那么请求将从链上离开,不同的实现对此有不同的反应。
还是拿实际问题来举例子,比如我现在手头有一批任务,然后我有一批员工,他们有各自擅长的技能,我想知道谁能完成我的任务,然后把任务交给那个人完成。面对这种问题,我们第一个想到的应该是循环,遍历每一个员工,然后找到合适的人。让我们用代码来实现一下:
// 任务类
var Assign = function (task) {
this.task = task; // 保存任务
}
// 发出请求的类
var WorkFlow = function (assign) {
this.assign = assign; // 保存接收的任务
}
WorkFlow.prototype = {
constructor: WorkFlow,
filterHandler: function (ex) { // 任务接收者的集合
for (var i = 0; i < ex.length; i++) {
if (ex[i].cando === this.assign.task) { // 如果某个接收者可以处理轻轻,则处理
ex[i].todo();
}
}
return // 否则返回
}
}
// 任务接受者类
var Executor = function (name, cando) {
this.name = name;
this.cando = cando; // 可处理的任务
}
Executor.prototype = {
constructor: Executor,
todo: function () {
document.write(this.name + '处理' + this.cando);
}
}
// 实例化
var ex1 = new Executor('小1', 'javascript');
var ex2 = new Executor('小2', 'sql');
var ex3 = new Executor('小3', 'css');
var ex4 = new Executor('小4', 'java');
var assign = new Assign('java');
var wf = new WorkFLow(assign);
wf.filterHandler([ex1, ex2, ex3, ex4]); // 小4处理java
我们可以发现一个问题,上面这种模式,任务的发出者需要拥有全部处理者的数组,发出者和处理者之间的耦合太强了,下面我们用责任链的模式对它进行重构:
// 任务类
var Assign = function (task) {
this.task = task; // 保存任务
}
// 发出请求的类
var WorkFlow = function (assign) {
this.assign = assign; // 保存接收的任务
}
WorkFlow.prototype = {
constructor: WorkFlow,
filterHandler: function (executor) { // 任务接收者的集合
if (this.assign.task === executor.cando) {
executor.todo();
} else {
arguments.callee.call(this, executor.successor); // 递归调用下一个接收者
}
return // 都不符合则返回
}
}
// 任务接受者类
var Executor = function (name, cando) {
this.name = name;
this.cando = cando; // 可处理的任务
this.successor = null // 用来保存下级对象的引用
}
Executor.prototype = {
constructor: Executor,
todo: function () {
document.write(this.name + '处理' + this.cando);
},
// 添加下级处理者的函数
setSuccessor: function (successor) {
this.successor = successor;
}
}
// 实例化
var ex1 = new Executor('小1', 'javascript');
var ex2 = new Executor('小2', 'sql');
var ex3 = new Executor('小3', 'css');
var ex4 = new Executor('小4', 'java');
ex1.setSuccessor(ex2);
ex2.setSuccessor(ex3);
ex3.setSuccessor(ex4);
var assign = new Assign('java');
var wf = new WorkFLow(assign);
// 只需要传递第一个接收者
wf.filterHandler(ex1); // 小4处理java
十四、策略模式
策略模式是指:一个方法,在面对不同情况时,会采用不同的解决策略。比如我现在有一个人的类,类里面有一个跑的方法:
var Person = function (fromWhere, toWhere) {
this.fromWhere = fromWhere;
this.toWhere = toWhere;
}
Person.prototype.run = function () {
}
现在有一个问题,我这个跑的方法不想写死,我在不同的情况下,想要采用不同的策略。这个时候就用到了策略模式,我们可以建立一个单例:
var strategy = {
slow: function (distance) { alert('慢速策略,耗时' + distance * 2 + '小时') };
nomal: function (distance) { alert('普通策略,耗时' + distance + '小时') };
fast: function (distance) { alert('快速策略,耗时' + distance / 2 + '小时') };
}
然后这样写run方法:
Person.prototype.run = function (chooseStrategy) { // 参数是一个函数
chooseStrategy(this.toWhere - this.fromWhere);
}
此时,我们就可以采用不同的策略了:
var p1 = new Person(0, 20);
p1.run(strategy.slow); // 慢速策略,耗时40小时
p1.run(strategy.nomal); // 普通策略,耗时20小时
p1.run(strategy.fast); // 快速策略,耗时10小时
十五、模板模式
模板模式,说白了就是子类重写父类的某个方法,其他方法还是用父类的,就是js的多态,就拿做果汁为例吧:
function Fruit () {};
Fruit.prototype = {
constructor: Fruit,
make: function () {
// 烧开水
this.water();
// 放水果
this.cailiao();
// 搅拌
this.jiaobao();
// 给用户喝
this.drink();
},
water: function () { console.log('烧开水') };
cailiao: function () { throw new Error('必须子类重写该方法') };
jiaoban: function () { console.log('搅拌') };
drink: function () { console.log('给用户喝') };
}
显然,材料不能写死,应该是各不相同,这时候就用到了模板模式:
function Apple () {};
// 先继承父类
Apple.prototype = new Fruit();
// 然后重写放水果的方法
Apple.prototype.cailiao = function () { console.log('放入苹果') };
var a = new Apple();
a.make();
function Pear () {};
// 先继承父类
Pear.prototype = new Fruit();
// 然后重写放水果的方法
Pear.prototype.cailiao = function () { console.log('放入梨子') };
var p = new Pear();
p.make();