Fork me on GitHub

设计模式

设计模式

前端常用的几种设计模式

概念

设计模式是一套被反复使用的代码设计经验的总结,代表了最佳实践。

设计原则

  • S - Single Responsibility Principle 单一职责设计模式
    • 一个程序只做好一件事
    • 如果功能过于复杂就拆开,每个部分保持独立
  • O - OpenClosed Principle 开放/封闭模式
    • 对扩展开放,对修改封闭
    • 增加需求时,扩展新代码,而非修改已有代码
  • L - LisKov Substitution Principle 里式替换原则
    • 子类能覆盖父类
    • 父类能出现的地方子类就能出现
  • I - Interface segregation Principle 接口隔离原则
    • 保持接口的单一独立
    • 类似单一原则,这里更关注接口
  • D - Dependency Inversion Principle 依赖倒转原则
    • 面向接口编程,依赖于抽象而不依赖于具象
    • 使用方只关注接口而不关注具体实现

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

定义一对一或一对多的依赖关系,当发布者发生变化时,订阅方都会收到通知。
异步编程中可以利用这种模式传递回调函数。比如请求成功或错误等事件。无需关系异步操作运行的内部状态,只需要订阅异步执行完成这个节点。
例子:DOM 节点绑定事件函数,这时 DOM 节点就是发布者,事件函数就是订阅者,

1
2
3
4
const btn=document.getElementById('btn');
btn.addEventListener('click',function(event){
console.log('按钮被点击')
})
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
class Publish {
constructor() {
this.subscribers = {}; //缓存订阅者
}

/*
* topic 标识订阅者
callback
*/
subscribe(topic, callback) {

if (typeof callback !== 'function') {
return '回调必须是函数!'
}

let callbacks = this.subscribers[topic];

if (!callbacks) {
this.subscribers[topic] = [callback]
} else {
callbacks.push(callback)
}

}

//触发订阅的事件
publish(topic, ...args) {
let callbacks = this.subscribers[topic] || [];
callbacks.forEach(callback => callback.apply(this, args))
}

//取消订阅
unsubscribe(topic, fn) {
let callbacks = this.subscribers[topic];
if (!callbacks) {
return '未注册事件'
} else {
this.subscribers[topic] = callbacks.filter(callback => callback !== fn)
}
}
}


let pub = new Publish();
const fn1 = (val) => {
console.log('test1', val)
}
const fn2 = (val) => {
console.log('test2', val)
}
pub.subscribe('test', fn1)
pub.subscribe('test', fn2)
pub.unsubscribe('test', fn1)
pub.publish('test', 111)

单例模式

确保一个类只有唯一一个实例,并且提供一个访问它的全局访问点。
举例:Window,全局缓存,全局状态管理
单例模式只实例化一次,多次调用实例化函数返回的都是第一次创建的实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//单例模式
let 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
}
let a = Singleton.getInstance('aa');
let b = Singleton.getInstance('bb');
console.log(a === b)

实际例子

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
 (function () {
//管理单例的逻辑代码,如果没有数据则创建,有数据则返回
var getSingle = function (fn) { //参数为创建对象的方法
var result;
return function () { //判断是Null或赋值
console.log(result)
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
17
18
19
20
21
22
23
24
25
26
27
28
29
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 User();
default:
throw new Error('参数错误,可选参数:superAdmin、admin、user')
}
}
let admin = UserFactory('admin');
  • 工厂方法 (创建多类对象)
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
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

let VehicleFactory = function (subType, superType) {
//判断抽象工厂中是否有该抽象类
if (typeof VehicleFactory[superType] === 'function') {
//缓存类
function F() {};
//继承父类属性和方法
F.prototype = new VehicleFactory[superType]();
subType.prototype = new F();
//将子类constructor指向子类
subType.constructor = subType
} else {
//不存在抽象类
throw new Error('未创建该抽象类')
}
}
//抽象一个汽车
VehicleFactory.Car = function () {
this.type = 'Car';
}
VehicleFactory.Car.prototype = {
getPrice: function () {
return new Error('抽象方法不能调用');
},
getSpeed: function () {
return new Error('抽象方法不能调用');
}
}
let BMW = function (price, speed) {
this.price = price;
this.speed = speed
}
VehicleFactory(BMW, 'Car')
BMW.prototype.getPrice = function () {
return this.price
};
BMW.prototype.getSpeed = function () {
return this.speed
};
let one = new BMW('1000', '100')

适配器模式

解决两个接口不兼容的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Plug {
getName() {
return '港版插头'
}
}

class Target {
constructor() {
this.plug = new Plug()
}
getName() {
return this.plug.getName() + ' 适配器转二脚插头'
}
}

let target = new Target()
target.getName() // 港版插头 适配器转二脚插头

例如在项目中拿到的时间是时间戳,我们要展示成日期,就需要一个转换函数。

代理模式

代理是为了控制对象的访问,不让外部对象直接访问到对象。

  • 图片懒加载

  • 缓存代理 (缓存请求结果、计算结果)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 const multi = function () {
let a = 1;
console.log('a', a)
for (let i = 0; i < arguments.length; i++) {
a *= arguments[i]
}
return a
}
//创建缓存代理
const createProxyFactory = function (fn) {
let cache = {}; //保存计算结果
return function () {
let args = Array.from(arguments).join(','); //将参数变成字符串,作为缓存对象的key
if (args in cache) {
return cache[args]
} else {
return cache[args] = fn.apply(this, arguments);
}
}
}
//使用代理对象创建函数
const proxyMulti = createProxyFactory(multi);
console.log(proxyMulti(1, 2, 3, 4))
  • 虚拟代理
    某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建
    例:使用虚拟代理实现图片懒加载
    1. 创建了一个 Image 对象,并为其绑定了 onload 事件。
    2. 将 imgNode 先设置为 ‘./loading.gif’ 加载的菊花图。
    3. 当 Image 对象加载完真实的图片,也就是上文的 ‘./reality.png’ ,将 imgNode 设置为 ‘./reality.png’。
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
const imgFunc = (
function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc(src) {
imgNode.src = src
}
}
}
)()
//代理对象
const proxyImage = (
function () {
const img = new Image();
img.onload = function () {
imgFunc.setSrc(this.src)
};
return {
setSrc(src) {
imgFunc.setSrc('./loading.gif');
img.src = src
}
}
}
)()
proxyImage.setSrc('./realImg.jpg')

外观模式

提供一个接口,隐藏内部的逻辑,更加方便外部调用
用于封装 JS 类库,通过封装一些接口用于兼容多浏览器

  • 写一个通用的事件侦听器函数
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
const EventUtils = {
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 添加事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},

// 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},

// 获取事件目标
getTarget: function(event) {
return event.target || event.srcElement;
},

// 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
getEvent: function(event) {
return event || window.event;
},

// 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},

// 取消事件的默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
  • 兼容浏览器阻止冒泡、默认事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let N = window.N || {};
    N.tools = {
    stopPropagation(e) {
    if (e.stopPropagation) {
    e.stopPropagation();
    } else {
    e.cancelBubble = true;
    }
    },
    preventDefault(e) {
    if (e.preventDefault) {
    e.preventDefault();
    } else {
    e.returnValue = false;
    }
    },
    stopEvent(e) {
    this.stopPropagation(e);
    this.preventDefault(e);
    }
    };

装饰者模式

不需要改变已有的接口,作用是给对象添加功能。
类装饰器,属性装饰器
ES7 的装饰器语法

1
2
3
4
5
6
7
8
9
10
11
12
13
function readonly(target, key, descriptor) {
descriptor.writable = false
return descriptor
}

class Test {
@readonly
name = 'yck'
}

let t = new Test()

t.yck = '111' // 不可修改
-------------本文结束感谢阅读-------------