目录

  1. 设计模式
    1. 工程模式
      1. 简单工厂模式(创建单一对象,需要的类比较少)
      2. 工厂方法模式(创建多类对象,需要的类比较多)
      3. 抽象工厂模式(创建父类,子类继承父类,具体实现在子类)
    2. 单例模式
      1. 基础例子
      2. 实践例子
    3. 观察者模式(发布-订阅模式)
      1. 基础例子
      2. 实践例子
    4. 策略模式
      1. 代码实现
    5. 模板模式
      1. 代码实现
    6. 代理模式
    7. 外观模式

转载:
  前端常用的设计模式
  常用的 JavaScript 设计模式

设计模式

  设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
  使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

工程模式

  定义: 将其成员对象的实例化推迟到子类来实现的类
  需求: 创建对象的流程赋值的时候,比如依赖很多设置文件等;处理大量具有相同属性的小对象,注:不能滥用
  优点: 不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中
  缺点: 当工厂增加到一定程度的时候,提升了代码的复杂度,可读性下降。而且没有解决对象的识别问题,即怎么知道一个对象的类型。

简单工厂模式(创建单一对象,需要的类比较少)

  使用一个类,通常为单体,来生成实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let UserFactory = function (role) {
function SuperAdmin() {
this.name = "超级管理员",
this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
}
function Admin() {
this.name = "管理员",
this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
}
function NormalUser() {
this.name = '普通用户',
this.viewPage = ['首页', '通讯录', '发现页']
}

switch (role) {
case 'superAdmin':
return new SuperAdmin();
break;
case 'admin':
return new Admin();
break;
case 'user':
return new NormalUser();
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user');
}
}

工厂方法模式(创建多类对象,需要的类比较多)

  为方便后续新增类方便,只需改一处代码,封装了工厂方法而已。并且把类都放在工厂类原型中实现。
  将其成员对象的实列化推到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。
  父类只对创建过程中的一般性问题进行处理,这些处理会被子类继承,子类之间是相互独立的,具体的业务逻辑会放在子类中进行编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//安全模式创建的工厂方法函数
let UserFactory = function(role) {
if(this instanceof UserFactory) {
var s = new this[role]();
return s;
} else {
return new UserFactory(role);
}
}

//工厂方法函数的原型中设置所有对象的构造函数
UserFactory.prototype = {
SuperAdmin: function() {
this.name = "超级管理员",
this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
},
Admin: function() {
this.name = "管理员",
this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
},
NormalUser: function() {
this.name = '普通用户',
this.viewPage = ['首页', '通讯录', '发现页']
}
}

//调用
let superAdmin = UserFactory('SuperAdmin');
let admin = UserFactory('Admin')
let normalUser = UserFactory('NormalUser')

抽象工厂模式(创建父类,子类继承父类,具体实现在子类)

  抽象工厂其实是实现子类继承父类的方法,只是一个方法。
  抽象工厂模式一般用在多人协作的超大型项目中,并且严格的要求项目以面向对象的思想进行完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 抽象工厂方法
var VehicleFatory = 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('抽象方法不能调用'); },
getSpeed: function() { return new Error('抽象方法不能调用'); }
};

// 货车抽象类
VehicleFactory.Truck = function() {
this.type = 'truck';
};
VehicleFactory.Truck.prototype = {
getPrice: function() { return new Error('抽象方法不能调用'); },
getSpeed: function() { return new Error('抽象方法不能调用'); }
};

// 创建产品子类继承相应的产品簇抽象类
// 宝马汽车子类
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 };

// 公交车...
// 货车...

单例模式

  定义:是保证一个类只有一个实例,并且提供一个访问它的全局访问点。
  需求:一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象、登录浮窗等。
  实现:用一个变量标识当前是否已经为某个类创建过对象,如果是,则在下一次获取这个类的实例时,直接返回之前创建的对象。
  优点:
    可以用来划分命名空间,减少全局变量的数量
    可以被实例化,且实例化一次,再次实例化生成的也是第一个实例

基础例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 单例模式
var Singleton = function(name){
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function(){
return this.name;
};
// 获取实例对象
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
// 测试单例模式的实例
var a = Singleton.getInstance("aa");
var b = Singleton.getInstance("bb");

console.log(a===b) // true
实践例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function () {
//管理单例的逻辑代码,如果没有数据则创建,有数据则返回
var getSingle = function(fn){ //参数为创建对象的方法
var result;
return function(){ //判断是Null或赋值
return result || (result = fn.apply(this,arguments));
};
};
//创建登录窗口方法
var createLoginLayer = function(){
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
//单例方法
var createSingleLoginLayer = getSingle(createLoginLayer);

//使用惰性单例,进行创建
document.getElementById('loginBtn').onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
})()

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

  定义:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新,也被称为是发布订阅模式。
  需求:当一个对象的状态发生变化时,所有依赖于他的对象都将得到通知。
  优点:时间上的解耦,对象之间的解耦。当我们需要维护相关对象的一致性的时候,使用观察者模式,,就可以避免对象之间的紧密耦合。例如,一个对象可以通知另外一个对象,而不需要知道这个对象的信息。
  缺点:在发布/订阅模式中,如果我们需要将发布者同订阅者上解耦,将会在一些情况下,导致很难确保我们应用中的特定部分按照我们预期的那样正常工作。也就是说它的优点也可能是它的缺点
  实现:
    首先,指定好谁充当发布者
    然后,给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
    最后,发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数
    退订(比如不想再接收到这些订阅的信息了,就可以取消掉)

基础例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var salesOffices = {};                           // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function( fn ){ // 增加订阅者
this.clientList.push( fn ); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function(){ // 发布消息
for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
fn.apply( this, arguments ); // arguments 是发布消息时带上的参数
}
};
//调用
salesOffices.listen( function( price, squareMeter ){//订阅消息
console.log( '价格= ' + price );
console.log( 'squareMeter= ' + squareMeter );
});
salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米
实践例子

  登录页面登录后,会需要刷新各个模块的信息(头像、nav)这类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
var ObserverEvent = (function () {
var clientList = [], listen, trigger, remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments), fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
for (var l = fns.length - 1; l >= 0; l--) {
var _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
}
};
return {
listen:listen,
trigger:trigger,
remove:remove
}
})();
ObserverEvent.listen('squareMeter88', fn1 = function (price) {
console.log('价格=' + price);
});
ObserverEvent.listen('squareMeter100', function (price) {
console.log('价格=' + price);
});
ObserverEvent.trigger('squareMeter88', 200000);

//刷新模块信息
var header = (function () {
ObserverEvent.listen('loginSucc', function (data) {
header.setAvatar(data.avatar);
});
return {
setAvatar: function (data) {
console.log(data + "设置header成功");
}
}
})();
var nav = (function () {
ObserverEvent.listen('loginSucc', function (data) {
nav.setAvatar(data.avatar)
});
return {
setAvatar: function (data) {
console.log(data + '设置nav成功');
}
}
})();
var data = {};
data.avatar = "参数";
ObserverEvent.trigger('loginSucc', data);

策略模式

  策略模式指的是定义一些列的算法,把他们一个个封装起来,目的就是将算法的使用与算法的实现分离开来。说白了就是以前要很多判断的写法,现在把判断里面的内容抽离开来,变成一个个小的个体。

代码实现

  代码情景为超市促销,vip为5折,老客户3折,普通顾客没折,计算最后需要支付的金额。
  没有使用策略模式的情况:

1
2
3
4
5
6
7
8
9
10
11
function Price(personType, price) {
//vip 5 折
if (personType == 'vip') {
return price * 0.5;
}
else if (personType == 'old'){ //老客户 3 折
return price * 0.3;
} else {
return price; //其他都全价
}
}

  不足之处:不好的地方,当我有其他方面的折扣时,又或者我活动的折扣时经常变化的,这样就要不断的修改if..else里面的条件了。而且也违背了设计模式的一个原则:对修改关闭,对扩展开放的原则;
  使用策略模式之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 对于vip客户
function vipPrice() {
this.discount = 0.5;
}
vipPrice.prototype.getPrice = function(price) {
  return price * this.discount;
}
// 对于老客户
function oldPrice() {
this.discount = 0.3;
}
oldPrice.prototype.getPrice = function(price) {
return price * this.discount;
}
// 对于普通客户
function Price() {
this.discount = 1;
}
Price.prototype.getPrice = function(price) {
return price ;
}
// 上下文,对于客户端的使用
function Context() {
this.name = '';
this.strategy = null;
this.price = 0;
}
Context.prototype.set = function(name, strategy, price) {
this.name = name;
this.strategy = strategy;
this.price = price;
}
Context.prototype.getResult = function() {
console.log(this.name + ' 的结账价为: ' + this.strategy.getPrice(this.price));
}

var context = new Context();
var vip = new vipPrice();
context.set ('vip客户', vip, 200);
context.getResult(); // vip客户 的结账价为: 100

var old = new oldPrice();
context.set ('老客户', old, 200);
context.getResult(); // 老客户 的结账价为: 60

var Price = new Price();
context.set ('普通客户', Price, 200);
context.getResult(); // 普通客户 的结账价为: 200

  通过策略模式,使得客户的折扣与算法解藕,又使得修改跟扩展能独立的进行,不影到客户端或其他算法的使用;

模板模式

  定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 通俗的讲,就是将一些公共方法封装到父类,子类可以继承这个父类,并且可以在子类中重写父类的方法,从而实现自己的业务逻辑。

代码实现

  比如前端面试,基本包括笔试,技术面试,领导面试,HR面试等,但是每个公司的笔试题,技术面可能不一样,也可能一样,一样的就继承父类的方法,不一样的就重写父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var Interview = function(){};
// 笔试
Interview.prototype.writtenTest = function(){
console.log("这里是前端笔试题");
};
// 技术面试
Interview.prototype.technicalInterview = function(){
console.log("这里是技术面试");
};
// 领导面试
Interview.prototype.leader = function(){
console.log("领导面试");
};
// 领导面试
Interview.prototype.HR = function(){
console.log("HR面试");
};
// 等通知
Interview.prototype.waitNotice = function(){
console.log("等通知啊,不知道过了没有哦");
};
// 代码初始化
Interview.prototype.init = function(){
this.writtenTest();
this.technicalInterview();
this.leader();
this.HR();
this.waitNotice();
};

// 阿里巴巴的笔试和技术面不同,重写父类方法,其他继承父类方法。
var AliInterview = function(){};
AliInterview.prototype = new Interview();

// 子类重写方法 实现自己的业务逻辑
AliInterview.prototype.writtenTest = function(){
console.log("阿里的技术题就是难啊");
}
AliInterview.prototype.technicalInterview = function(){
console.log("阿里的技术面就是叼啊");
}
var AliInterview = new AliInterview();
AliInterview.init();

// 阿里的技术题就是难啊
// 阿里的技术面就是叼啊
// 领导面试
// HR面试
// 等通知啊,不知道过了没有哦

应用场景:
  模板模式主要应用在一些代码刚开要一次性实现不变的部分。但是将来页面有修改,需要更改业务逻辑的部分或者重新添加新业务的情况。主要是通过子类来改写父类的情况,其他不需要改变的部分继承父类。   

代理模式

概念:
  代理模式的中文含义就是帮别人做事,javascript的解释为:把对一个对象的访问, 交给另一个代理对象来操作.
代码实现:
  比如我们公司的补打卡是最后是要交给大boss来审批的,但是公司那么多人,每天都那么多补打卡,那大boss岂不是被这些琐事累死。所以大boss下会有一个助理,来帮忙做这个审批,最后再将每个月的补打卡统一交给大boss看看就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 补打卡事件
var fillOut = function (lateDate) {

this.lateDate = lateDate;
};

// 这是bigBoss
var bigBoss = function (fillOut) {

this.state = function (isSuccess) {
console.log("忘记打卡的日期为:" + fillOut.lateDate + ", 补打卡状态:" + isSuccess);
}
};
// 助理代理大boss 完成补打卡审批
var proxyAssis = function (fillOut) {

this.state = function (isSuccess) {
(new bigBoss(fillOut)).state(isSuccess); // 替bigBoss审批
}
};

// 调用方法:
var proxyAssis = new proxyAssis(new fillOut("2016-9-11"));
proxyAssis.state("补打卡成功");
// 忘记打卡的日期为:2016-9-11, 补打卡状态:补打卡成功

应用场景:
  比如图片的懒加载,我们就可以运用这种技术。在图片未加载完成之前,给个loading图片,加载完成后再替换成实体路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return function(src){
imgNode.src = src;
}
})();
// 代理模式
var ProxyImage = (function(){
var img = new Image();
img.onload = function(){
myImage(this.src);
};
return function(src) {
// 占位图片loading
myImage("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
img.src = src;
}
})();
// 调用方式

ProxyImage("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png"); // 真实要展示的图片

  当然,这种懒加载方法不用代理模式也是可以实现的,只是用代理模式。我们可以让 myImage 只做一件事,只负责将实际图片加入到页面中,而loading图片交给ProxyImage去做。从而降低代码的耦合度。因为当我不想用loading的时候,可以直接调用myImage 方法。也即是说假如我门不需要代理对象的话,直接可以换成本体对象调用该方法即可。

外观模式

  概念:
  外观模式是很常见。其实它就是通过编写一个单独的函数,来简化对一个或多个更大型的,可能更为复杂的函数的访问。也就是说可以视外观模式为一种简化某些内容的手段。
  说白了,外观模式就是一个函数,封装了复杂的操作。
代码实现:
  比如一个跨浏览器的ajax调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function ajaxCall(type,url,callback,data){
// 根据当前浏览器获取对ajax连接对象的引用
var xhr=(function(){
try {
// 所有现代浏览器所使用的标准方法
return new XMLHttpRequest();
}catch(e){}
// 较老版本的internet Explorer兼容
try{
return new ActiveXObject("Msxml2.XMLHTTP.6.0");
}catch(e){}
try{
return new ActiveXObject("Msxml2.XMLHTTP.3.0");

}catch(e){}
try{
return new ActiveXObject("Microsoft.XMLHTTP");
}catch(e){}

// 如果没能找到相关的ajax连接对象,则跑出一个错误。
throw new Error("Ajax not support in this browser.")

}()),
STATE_LOADED=4,
STATUS_OK=200;

// 一但从服务器收到表示成功的相应消息,则执行所给定的回调方法
xhr.onreadystatechange=function{
if(xhr.readyState !==STATE_LOADED){
return;
}
if(xhr.state==STATUS_OK){
callback(xhr.responseText);
}
}
// 使用浏览器的ajax连接对象来向所给定的URL发出相关的调用
xhr.open(type.toUpperCase(),url);
xhr.send(data);
}
// 使用方法
ajaxCall("get","/user/12345",function(rs){
alert('收到的数据为:'+rs);
})

应用场景:
  当需要通过一个单独的函数或方法来访问一系列的函数或方法调用,以简化代码库的其余内容,使得代码更容易跟踪管理或者更好的维护时,可以使用外观模式。其实我们平时代码中这种模式应该是用的比较多的。