# 设计模式
# 一、订阅/发布模式
pub/sub 这个应该⼤家⽤到最⼴的设计模式了,在这种模式中,并不是⼀个对象调⽤另⼀个对象的⽅法,⽽是⼀个对象订阅另⼀个对象的特定活动并在状态改变后获得通知。订阅者因此也成为观察者,⽽被观察的对象成为发布者或者主题。当发⽣了⼀个重要事件时候发布者会通知(调⽤)所有订阅者并且可能经常已事件对象的形式传递消息。
class Event {
constructor() {
this.callbacks = {}
}
$off(name) {
this.callbacks[name] = null
}
$emit(name, args) {
let cbs = this.callbacks[name]
if (cbs) {
cbs.forEach(c => {
c.call(this, args)
})
}
}
$on(name, fn) {
(this.callbacks[name] || (this.callbacks[name] = [])).push(fn)
}
}
let event = new Event()
event.$on('event1', function (arg) {
console.log('事件1', arg)
})
event.$on('event1', function (arg) {
console.log('⼜⼀个事件1', arg)
})
event.$on('event2', function (arg) {
console.log('事件2', arg)
})
event.$emit('event1', { name: 'js' })
event.$emit('event2', { name: '网络' })
event.$off('event1')
event.$emit('event1', { name: '设计模式' })
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
# 二、单例模式
单例模式的定义:保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。实现的⽅法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了⼀个类只有⼀个实例对象。
适⽤场景:⼀个单⼀对象。⽐如:弹窗,⽆论点击多少次,弹窗只应该被创建⼀次,实现起来也很简单,⽤⼀个变量缓存即可。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.model {
border: 1px solid black;
position: fixed;
width: 300px;
height: 300px;
top: 20%;
left: 50%;
margin-left: -150px;
text-align: center;
}
</style>
</head>
<body>
<div id="loginBtn">点我</div>
<script>
var getSingle = function (fn) {
var result;
return function () {
return result || (result = fn.apply(this, arguments));
}
};
var createLoginLayer = function () {
var div = document.createElement('div');
div.innerHTML = '我是登录浮窗';
div.className = 'model'
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';
};
</script>
</body>
</html>
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
# 三、策略模式
策略模式的定义:定义⼀系列的算法,把他们⼀个个封装起来,并且使他们可以相互替换。策略模式的⽬的就是将算法的实现分离开来。
⼀个基于策略模式的程序⾄少由两部分组成。第⼀个部分是⼀组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第⼆个部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某⼀个策略类。要做到这⼀点,说明Context中要维持对某个策略对象的引⽤。
# 举个栗⼦
奖⾦计算,绩效为 S 的⼈年 终奖有 4 倍⼯资,绩效为 A 的⼈年终奖有 3 倍⼯资,⽽绩效为 B 的⼈年终奖是 2 倍⼯资。
不使用策略
// 不使用策略
var calculateBonus = function (performanceLevel, salary) {
if (performanceLevel === 'S') {
return salary * 4;
}
if (performanceLevel === 'A') {
return salary * 3;
}
if (performanceLevel === 'B') {
return salary * 2;
}
};
calculateBonus('B', 20000); // 输出:40000
calculateBonus('S', 6000); // 输出:24000
2
3
4
5
6
7
8
9
10
11
12
13
14
使用策略
// 使用策略
var strategies = {
"S": function (salary) {
return salary * 4;
},
"A": function (salary) {
return salary * 3;
},
"B": function (salary) {
return salary * 2;
}
};
var calculateBonus = function (level, salary) {
if (level in strategies) {
return strategies[level](salary);
}
return salary;
};
console.log(calculateBonus('S', 20000));// 输出:80000
console.log(calculateBonus('A', 10000));// 输出:30000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 表单校验
不使用策略
// 正常写法
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
if (registerForm.userName.value === '') {
alert('⽤户名不能为空');
return false;
}
if (registerForm.password.value.length < 6) {
alert('密码⻓度不能少于 6 位');
return false;
}
if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
alert('⼿机号码格式不正确');
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用策略
var strategies = {
isNonEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) { // ⼿机号码格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
var Validator = function () {
this.cache = []; // 保存校验规则
};
Validator.prototype.add = function () {
var ary = rule.split(':');
this.cache.push(function () { //
var strategy = ary.shift();
ary.unshift(dom.value);
ary.push(errorMsg); //
return strategies[strategy].apply(dom, ary);
});
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if (msg) { // 如果有确切的返回值,说明校验没有通过
return msg;
}
}
};
var validataFunc = function () {
var validator = new Validator(); // 创建⼀个 validator 对象
/***************添加⼀些校验规则****************/
validator.add(registerForm.userName, 'isNonEmpty', '⽤户名不能为空');
validator.add(registerForm.password, 'minLength:6', '密码⻓度不能少于 6位');
validator.add(registerForm.phoneNumber, 'isMobile', '⼿机号码格式不正确');
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
var errorMsg = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
if (errorMsg) {
alert(errorMsg);
return false; // 阻⽌表单提交
}
};
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
# 四、代理模式
代理模式的定义:为⼀个对象提供⼀个代⽤品或占位符,以便控制对它的访问。
常⽤的虚拟代理形式:某⼀个花销很⼤的操作,可以通过虚拟代理的⽅式延迟到这种需要它的时候才去创建(例:使⽤虚拟代理实现图⽚懒加载)。
图⽚懒加载的⽅式:先通过⼀张loading图占位,然后通过异步的⽅式加载图⽚,等图⽚加载好了再把完成的图⽚加载到img标签⾥⾯。
var imgFunc = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
}
}
})();
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
imgFunc.setSrc(this.src);
}
return {
setSrc: function (src) {
imgFunc.setSrc('./loading,gif');
img.src = src;
}
}
})();
proxyImage.setSrc('./pic.png');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
假设我们在做⼀个⽂件同步的功能,当我们选中⼀个checkbox的时候,它对应的⽂件就会被同步到另外⼀台备⽤服务器上⾯。当⼀次选中过多时,会产⽣频繁的⽹络请求。将带来很⼤的开销。可以通过⼀个代理函数proxySynchronousFile来收集⼀段时间之内的请求, 最后⼀次性发送给服务器.
函数的节流防抖,也是一种代理模式。
# 五、中介者模式
中介者模式的定义:通过⼀个中介者对象,其他所有的相关对象都通过该中介者对象来通信,⽽不是相互引⽤,当其中的⼀个对象发⽣改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。
例如:现实⽣活中,航线上的⻜机只需要和机场的塔台通信就能确定航线和⻜⾏状态,⽽不需要和所有⻜机通信。同时塔台作为中介者,知道每架⻜机的⻜⾏状态,所以可以安排所有⻜机的起降和航线安排。
中介者模式适⽤的场景:例如购物⻋需求,存在商品选择表单、颜⾊选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。
redux,vuex 都属于中介者模式的实际应⽤,我们把共享的数据,抽离成⼀个单独的store, 每个都通过store这个中介来操作对象⽬的就是减少耦合。
# 六、装饰器模式
装饰者模式的定义:在不改变对象⾃身的基础上,在程序运⾏期间给对象动态地添加⽅法。常⻅应⽤,react的⾼阶组件, 或者react-redux中的@connect 或者⾃⼰定义⼀些⾼阶组件。
import React from 'react'
// 打印日志的功能
const withLog = Component => {
// 类组件
class NewComponent extends React.Component {
componentWillMount() {
console.time(`CompoentRender`)
console.log(`准备完毕了`)
}
render() {
return <Component {...this.props}></Component>
}
componentDidMount() {
console.timeEnd(`CompoentRender`)
console.log(`渲染完毕了`)
}
}
return NewComponent
}
export { withLog }
@withLog
class XX {
}
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
# 登陆案例
⽐如⻚⾯中有⼀个登录button,点击这个button会弹出登录浮层,与此同时要进⾏数据上报, 来统计有多少⽤户点击了这个登录 button。
不使用装饰器
var showLogin = function(){
console.log( '打开登录浮层' );
log( this.getAttribute( 'tag' ) );
}
var log = function( tag ){
console.log( '上报标签为: ' + tag );
(new Image).src = 'http:// xxx.com/report?tag=' + tag;
}
document.getElementById( 'button' ).onclick = showLogin;
2
3
4
5
6
7
8
9
使用装饰器
// 定义一个前置函数
Function.prototype.before = function (beforefn) {
var __self = this; // 保存原函数的引⽤
return function () { // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执⾏新函数,且保证 this 不被劫持,新函数接受的参数也会被原封不动地传⼊原函数,新函数在原函数之前执⾏
return __self.apply(this, arguments); // 执⾏原函数并返回原函数的执⾏结果,并且保证 this 不被劫持
}
}
//定义一个后置函数
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
};
var showLogin = function () {
console.log('打开登录浮层');
}
var log = function () {
console.log('上报标签为: ' + this.getAttribute('tag'));
}
showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
document.getElementById('button').onclick = showLogin;
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
装饰者模式和代理模式的结构看起来⾮常相像,这两种模式都描述了怎样为对象提供 ⼀定程度上的间接引⽤,它们的实现部分都保留了对另外⼀个对象的引⽤,并且向那个对象发送 请求。代理模式和装饰者模式最重要的区别在于它们的意图和设计⽬的。代理模式的⽬的是,当直接访问本体不⽅便或者不符合需要时,为这个本体提供⼀个替代者。本体定义了关键功能,⽽代理提供或拒绝对它的访问,或者在访问本体之前做⼀些额外的事情。装饰者模式的作⽤就是为对象动态加⼊⾏为。
其实Vue中的v-input,v-checkbox也可以认为是装饰器模式,对原⽣的input和checkbox做⼀层装饰。
# 七、外观模式
外观模式即让多个⽅法⼀起被调⽤涉及到兼容性,参数⽀持多格式,有很多这种代码,对外暴露统⼀的api,⽐如上⾯的$on⽀持数组,$off参数⽀持多个情况, 对⾯只⽤⼀个函数,内部判断实现。⾃⼰封装组件库经常看到
myEvent = {
stop(e) {
if (typeof e.preventDefault() === "function") {
e.preventDefault();
}
if (typeof e.stopPropagation() === "function") {
e.stopPropagation();
}
//for IE
if (typeof e.returnValue === "boolean") {
e.returnValue = false;
}
if (typeof e.cancelBubble === "boolean") {
e.cancelBubble = true;
}
},
addEvent(dom, type, fn) {
if (dom.addEventListener) {
dom.addEventListener(type, fn, false);
} else if (dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else {
dom['on' + type] = fn;
}
}
}
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
# 八、工厂模式
提供创建对象的接⼝,把成员对象的创建⼯作转交给⼀个外部对象,好处在于消除对象之间的耦合(也就是相互影响)。
常⻅的例⼦,我们的弹窗,message,对外提供的api,都是调⽤api,然后新建⼀个弹窗或者Message的实例,就是典型的⼯⼚模式。