1. 概念
设计模式是一套被重复应用的、少数人通晓的、通过分类编目标、代码设计教训的总结。应用设计模式是为了重用代码、让代码更容易被别人了解、保障代码可靠性。毫无疑问,设计模式于己于别人于零碎都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
2. 设计准则
S – Single Responsibility Principle 繁多职责准则
- 一个程序只做好一件事
- 如果性能过于简单就拆离开,每个局部放弃独立
O – OpenClosed Principle 凋谢 / 关闭准则
- 对扩大凋谢,对批改关闭
- 减少需要时,扩大新代码,而非批改已有代码
L – Liskov Substitution Principle 里氏替换准则
- 子类能笼罩父类
- 父类能呈现的中央子类就能呈现
I – Interface Segregation Principle 接口隔离准则
- 放弃接口的繁多独立
- 相似繁多职责准则,这里更关注接口
D – Dependency Inversion Principle 依赖倒转准则
- 面向接口编程,依赖于形象而不依赖于工具
- 应用方只关注接口而不关注具体类的实现
3. 设计模式的类型
- 结构型模式(Structural Patterns): 通过识别系统中组件间的简略关系来简化零碎的设计。
- 创立型模式(Creational Patterns): 解决对象的创立,依据理论状况应用适合的形式创建对象。惯例的对象创立形式可能会导致设计上的问题,或减少设计的复杂度。创立型模式通过以某种形式管制对象的创立来解决问题。
- 行为型模式(Behavioral Patterns): 用于辨认对象之间常见的交互模式并加以实现,如此,减少了这些交互的灵活性。
9 种前端常见的设计模式
1. 外观模式(Facade Pattern)
外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个对立的高层接口,使子系统更容易应用。简而言之外观设计模式就是把多个子系统中简单逻辑进行形象,从而提供一个更对立、更简洁、更易用的 API。很多咱们罕用的框架和库根本都遵循了外观设计模式,比方 JQuery 就把简单的原生 DOM 操作进行了形象和封装,并打消了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中咱们也会常常用到外观模式进行开发,只是咱们不自知而已。
-
兼容浏览器事件绑定
`let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent(‘on’ + ev, fn)
} else {
el[‘on’ + ev] = fn
}
};`
-
封装接口
`let myEvent = {
// …
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
`
场景
- 设计初期,应该要无意识地将不同的两个层拆散,比方经典的三层构造,在数据拜访层和业务逻辑层、业务逻辑层和表示层之间建设外观 Facade
- 在开发阶段,子系统往往因为一直的重构演变而变得越来越简单,减少外观 Facade 能够提供一个简略的接口,缩小他们之间的依赖。
- 在保护一个遗留的大型零碎时,可能这个零碎曾经很难保护了,这时候应用外观 Facade 也是十分适合的,为系零碎开发一个外观 Facade 类,为设计毛糙和高度简单的遗留代码提供比拟清晰的接口,让新零碎和 Facade 对象交互,Facade 与遗留代码交互所有的简单工作。
长处
- 缩小零碎相互依赖。
- 进步灵活性。
- 进步了安全性
毛病
- 不合乎开闭准则,如果要改货色很麻烦,继承重写都不适合。
2. 代理模式(Proxy Pattern)
是为一个对象提供一个代用品或占位符,以便管制对它的拜访
假如当 A 在情绪好的时候收到花,小明表白胜利的几率有 60%,而当 A 在情绪差的时候收到花,小明表白的成功率有限趋近于 0。小明跟 A 刚刚意识两天,还无奈分别 A 什么时候情绪好。如果不合时宜地把花送给 A,花 被间接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。然而 A 的敌人 B 却很理解 A,所以小明只管把花交给 B,B 会监听 A 的情绪变动,而后选 择 A 情绪好的时候把花转交给 A,代码如下:
`let Flower = function() {}
let xiaoming = {
sendFlower: function(target) {
let flower = new Flower()
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function(flower) {
console.log(‘ 收到花 ’+ flower)
},
listenGoodMood: function(fn) {
setTimeout(function() {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B)
`
场景
- HTML 元 素事件代理
`<ul id=”ul”>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script> let ul = document.querySelector(‘#ul’);
ul.addEventListener(‘click’, event => {
console.log(event.target);
});</script>
`
- ES6 的 proxy 阮一峰 Proxy
- jQuery.proxy()办法
长处
- 代理模式能将代理对象与被调用对象拆散,升高了零碎的耦合度。代理模式在客户端和指标对象之间起到一个中介作用,这样能够起到爱护指标对象的作用
- 代理对象能够扩大指标对象的性能;通过批改代理对象就能够了,合乎开闭准则;
毛病
- 解决申请速度可能有差异,非间接拜访存在开销
3. 工厂模式(Factory Pattern)
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化提早到了子类。而子类能够重写接口办法以便创立的时候指定本人的对象类型。
`class Product {
constructor(name) {
this.name = name
}
init() {
console.log(‘init’)
}
fun() {
console.log(‘fun’)
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create(‘p1’)
p.init()
p.fun()
`
场景
- 如果你不想让某个子系统与较大的那个对象之间造成强耦合,而是想运行时从许多子系统中进行筛选的话,那么工厂模式是一个现实的抉择
- 将 new 操作简略封装,遇到 new 的时候就应该思考是否用工厂模式;
- 须要依赖具体环境创立不同实例,这些实例都有雷同的行为, 这时候咱们能够应用工厂模式,简化实现的过程,同时也能够缩小每种对象所需的代码量,有利于打消对象间的耦合,提供更大的灵活性
长处
- 创建对象的过程可能很简单,但咱们只须要关怀创立后果。
- 构造函数和创建者拆散, 合乎“开闭准则”
- 一个调用者想创立一个对象,只有晓得其名称就能够了。
- 扩展性高,如果想减少一个产品,只有扩大一个工厂类就能够。
毛病
- 增加新产品时,须要编写新的具体产品类, 肯定水平上减少了零碎的复杂度
- 思考到零碎的可扩展性,须要引入形象层,在客户端代码中均应用形象层进行定义,减少了零碎的抽象性和了解难度
什么时候不必
- 当被利用到谬误的问题类型上时, 这一模式会给应用程序引入大量不必要的复杂性. 除非为创建对象提供一个接口是咱们编写的库或者框架的一个设计上指标, 否则我会倡议应用明确的结构器, 以防止不必要的开销。
- 因为对象的创立过程被高效的形象在一个接口前面的事实, 这也会给依赖于这个过程可能会有多简单的单元测试带来问题。
4. 单例模式(Singleton Pattern)
顾名思义,单例模式中 Class 的实例个数最多为 1。当须要一个对象去贯通整个零碎执行某些工作时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的应用,因为单例模式会引入全局状态,而一个衰弱的零碎应该防止引入过多的全局状态。
实现单例模式须要解决以下几个问题:
- 如何确定 Class 只有一个实例?
- 如何简便的拜访 Class 的惟一实例?
- Class 如何管制实例化的过程?
- 如何将 Class 的实例个数限度为 1?
咱们个别通过实现以下两点来解决上述问题:
- 暗藏 Class 的构造函数,防止屡次实例化
- 通过裸露一个 getInstance() 办法来创立 / 获取惟一实例
Javascript 中单例模式能够通过以下形式实现:
`// 单例结构器
const FooServiceSingleton = (function () {
// 暗藏的 Class 的构造函数
function FooService() {}
// 未初始化的单例对象
let fooService;
return {
// 创立 / 获取单例对象的函数
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
`
实现的关键点有:
- 应用 IIFE 创立部分作用域并即时执行;
getInstance()
为一个 闭包,应用闭包保留部分作用域中的单例对象并返回。
咱们能够验证下单例对象是否创立胜利:
`const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
`
场景例子
- 定义命名空间和实现分支型办法
- 登录框
- vuex 和 redux 中的 store
长处
- 划分命名空间,缩小全局变量
- 加强模块性,把本人的代码组织在一个全局变量名下,放在繁多地位,便于保护
- 且只会实例化一次。简化了代码的调试和保护
毛病
- 因为单例模式提供的是一种单点拜访,所以它有可能导致模块间的强耦合
- 从而不利于单元测试。无奈独自测试一个调用了来自单例的办法的类,而只能把它与那个单例作为一
- 个单元一起测试。
5. 策略模式(Strategy Pattern)
策略模式简略形容就是:对象有某个行为,然而在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们能够相互替换
`<html>
<head>
<title> 策略模式 - 校验表单 </title>
<meta content=”text/html; charset=utf-8″ http-equiv=”Content-Type”>
</head>
<body>
<form id = “registerForm” method=”post” action=”http://xxxx.com/api/register”>
用户名:<input type=”text” name=”userName”>
明码:<input type=”text” name=”password”>
手机号码:<input type=”text” name=”phoneNumber”>
<button type=”submit”> 提交 </button>
</form>
<script type=”text/javascript”> // 策略对象
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === ”) {
return errorMsg;
}
},
isNoSpace: function (value, errorMsg) {
if (value.trim() === ”) {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.trim().length < length) {
return errorMsg;
}
},
maxLength: function (value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])d{8}$/.test(value)) {
return errorMsg;
}
}
}
// 验证类
class Validator {
constructor() {
this.cache = []
}
add(dom, rules) {
for(let i = 0, rule; rule = rules[i++];) {
let strategyAry = rule.strategy.split(‘:’)
let errorMsg = rule.errorMsg
this.cache.push(() => {
let strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
}
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// 调用代码
let registerForm = document.getElementById(‘registerForm’)
let validataFunc = function() {
let validator = new Validator()
validator.add(registerForm.userName, [{
strategy: ‘isNoEmpty’,
errorMsg: ‘ 用户名不可为空 ’
}, {
strategy: ‘isNoSpace’,
errorMsg: ‘ 不容许以空白字符命名 ’
}, {
strategy: ‘minLength:2’,
errorMsg: ‘ 用户名长度不能小于 2 位 ’
}])
validator.add(registerForm.password, [ {
strategy: ‘minLength:6’,
errorMsg: ‘ 明码长度不能小于 6 位 ’
}])
validator.add(registerForm.phoneNumber, [{
strategy: ‘isMobile’,
errorMsg: ‘ 请输出正确的手机号码格局 ’
}])
return validator.start()
}
registerForm.onsubmit = function() {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
} </script>
</body>
</html>
`
场景例子
- 如果在一个零碎外面有许多类,它们之间的区别仅在于它们的 ’ 行为 ’,那么应用策略模式能够动静地让一个对象在许多行为中抉择一种行为。
- 一个零碎须要动静地在几种算法中抉择一种。
- 表单验证
长处
- 利用组合、委托、多态等技术和思维,能够无效的防止多重条件抉择语句
- 提供了对凋谢 - 关闭准则的完满反对,将算法封装在独立的 strategy 中,使得它们易于切换,了解,易于扩大
- 利用组合和委托来让 Context 领有执行算法的能力,这也是继承的一种更轻便的代替计划
毛病
- 会在程序中减少许多策略类或者策略对象
- 要应用策略模式,必须理解所有的 strategy,必须理解各个 strategy 之间的不同点,这样能力抉择一个适合的 strategy
6. 迭代器模式(Iterator Pattern)
如果你看到这,ES6 中的迭代器 Iterator 置信你还是有点印象的,下面第 60 条曾经做过简略的介绍。迭代器模式简略的说就是提供一种办法程序一个聚合对象中各个元素,而又不裸露该对象的外部示意。
迭代器模式解决了以下问题:
- 提供统一的遍历各种数据结构的形式,而不必理解数据的内部结构
- 提供遍历容器(汇合)的能力而无需扭转容器的接口
一个迭代器通常须要实现以下接口:
- hasNext():判断迭代是否完结,返回 Boolean
- next():查找并返回下一个元素
为 Javascript 的数组实现一个迭代器能够这么写:
`const item = [1, ‘red’, false, 3.14];
function Iterator(items) {
this.items = items;
this.index = 0;
}
Iterator.prototype = {
hasNext: function () {
return this.index < this.items.length;
},
next: function () {
return this.items[this.index++];
}
}
`
验证一下迭代器是否工作:
`const iterator = new Iterator(item);
while(iterator.hasNext()){
console.log(iterator.next());
}
// 输入:1, red, false, 3.14
`
ES6 提供了更简略的迭代循环语法 for…of,应用该语法的前提是操作对象须要实现 可迭代协定(The iterable protocol),简略说就是该对象有个 Key 为 Symbol.iterator 的办法,该办法返回一个 iterator 对象。
比方咱们实现一个 Range 类用于在某个数字区间进行迭代:
`function Range(start, end) {
return {
[Symbol.iterator]: function () {
return {
next() {
if (start < end) {
return {value: start++, done: false};
}
return {done: true, value: end};
}
}
}
}
}
`
验证一下:
`for (num of Range(1, 5)) {
console.log(num);
}
// 输入:1, 2, 3, 4
`
7. 观察者模式(Observer Pattern)
观察者模式 又称 公布 - 订阅模式 (Publish/Subscribe Pattern),是咱们常常接触到的设计模式,日常生活中的利用也亘古未有,比方你订阅了某个博主的频道,当有内容更新时会收到推送;又比方 JavaScript 中的事件订阅响应机制。观察者模式的思维用一句话形容就是: 被察看对象(subject)保护一组观察者(observer),当被察看对象状态扭转时,通过调用观察者的某个办法将这些变动告诉到观察者。
观察者模式中 Subject 对象个别须要实现以下 API:
- subscribe(): 接管一个观察者 observer 对象,使其订阅本人
- unsubscribe(): 接管一个观察者 observer 对象,使其勾销订阅本人
- fire(): 触发事件,告诉到所有观察者
用 JavaScript 手动实现观察者模式:
`// 被观察者
function Subject() {
this.observers = [];
}
Subject.prototype = {
// 订阅
subscribe: function (observer) {
this.observers.push(observer);
},
// 勾销订阅
unsubscribe: function (observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;
})
},
// 事件触发
fire: function () {
this.observers.forEach(observer => {
observer.call();
});
}
}
`
验证一下订阅是否胜利:
`const subject = new Subject();
function observer1() {
console.log(‘Observer 1 Firing!’);
}
function observer2() {
console.log(‘Observer 2 Firing!’);
}
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();
// 输入:
Observer 1 Firing!
Observer 2 Firing!
`
验证一下勾销订阅是否胜利:
`subject.unsubscribe(observer2);
subject.fire();
// 输入:
Observer 1 Firing!
`
场景
- DOM 事件
`document.body.addEventListener(‘click’, function() {
console.log(‘hello world!’);
});
document.body.click()
`
- vue 响应式
长处
- 反对简略的播送通信,主动告诉所有曾经订阅过的对象
- 指标对象与观察者之间的形象耦合关系能独自扩大以及重用
- 减少了灵活性
- 观察者模式所做的工作就是在解耦,让耦合的单方都依赖于形象,而不是依赖于具体。从而使得各自的变动都不会影响到另一边的变动。
毛病
- 适度应用会导致对象与对象之间的分割弱化,会导致程序难以跟踪保护和了解
8. 中介者模式(Mediator Pattern)
在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的形式,使得这些对象不用间接相互作用,而是由中介者协调它们之间的交互,从而使它们能够涣散偶合。当某些对象之间的作用产生扭转时,不会立刻影响其余的一些对象之间的作用,保障这些作用能够彼此独立的变动。
中介者模式和观察者模式有肯定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是解决同级对象之间的交互,而观察者模式是解决 Observer 和 Subject 之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能间接交换,而是要通过中介去筛选匹配再决定谁和谁见面。
场景
- 例如购物车需要,存在商品抉择表单、色彩抉择表单、购买数量表单等等,都会触发 change 事件,那么能够通过中介者来转发解决这些事件,实现各个事件间的解耦,仅仅保护中介者对象即可。
`var goods = {// 手机库存
‘red|32G’: 3,
‘red|64G’: 1,
‘blue|32G’: 7,
‘blue|32G’: 6,
};
// 中介者
var mediator = (function() {
var colorSelect = document.getElementById(‘colorSelect’);
var memorySelect = document.getElementById(‘memorySelect’);
var numSelect = document.getElementById(‘numSelect’);
return {
changed: function(obj) {
switch(obj){
case colorSelect:
//TODO
break;
case memorySelect:
//TODO
break;
case numSelect:
//TODO
break;
}
}
}
})();
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numSelect.onchange = function() {
mediator.changed(this);
};
`
- 聊天室里
聊天室成员类:
“function Member(name) {
this.name = name;
this.chatroom = null;
}
Member.prototype = {
// 发送音讯
send: function (message, toMember) {
this.chatroom.send(message, this, toMember);
},
// 接管音讯
receive: function (message, fromMember) {
console.log(${fromMember.name} to ${this.name}: ${message}
);
}
}
“
聊天室类:
`function Chatroom() {
this.members = {};
}
Chatroom.prototype = {
// 减少成员
addMember: function (member) {
this.members[member.name] = member;
member.chatroom = this;
},
// 发送音讯
send: function (message, fromMember, toMember) {
toMember.receive(message, fromMember);
}
}
`
测试一下:
`const chatroom = new Chatroom();
const bruce = new Member(‘bruce’);
const frank = new Member(‘frank’);
chatroom.addMember(bruce);
chatroom.addMember(frank);
bruce.send(‘Hey frank’, frank);
// 输入:bruce to frank: hello frank
`
长处
- 使各对象之间耦合涣散,而且能够独立地扭转它们之间的交互
- 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
- 如果对象之间的简单耦合度导致保护很艰难,而且耦合度随我的项目变动增速很快,就须要中介者重构代码
毛病
- 零碎中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象常常是微小的。中介 者对象本身往往就是一个难以保护的对象。
9. 访问者模式(Visitor Pattern)
访问者模式 是一种将算法与对象构造拆散的设计模式,艰深点讲就是:访问者模式让咱们可能在不扭转一个对象构造的前提下可能给该对象减少新的逻辑,新增的逻辑保留在一个独立的访问者对象中。访问者模式罕用于拓展一些第三方的库和工具。
`// 访问者
class Visitor {
constructor() {}
visitConcreteElement(ConcreteElement) {
ConcreteElement.operation()
}
}
// 元素类
class ConcreteElement{
constructor() {
}
operation() {
console.log(“ConcreteElement.operation invoked”);
}
accept(visitor) {
visitor.visitConcreteElement(this)
}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)
`
访问者模式的实现有以下几个因素:
- Visitor Object:访问者对象,领有一个
visit()
办法 - Receiving Object:接管对象,领有一个
accept()
办法 - visit(receivingObj):用于 Visitor 接管一个
Receiving Object
- accept(visitor):用于
Receving Object
接管一个 Visitor,并通过调用Visitor
的visit()
为其提供获取Receiving Object
数据的能力
简略的代码实现如下:
`Receiving Object:
function Employee(name, salary) {
this.name = name;
this.salary = salary;
}
Employee.prototype = {
getSalary: function () {
return this.salary;
},
setSalary: function (salary) {
this.salary = salary;
},
accept: function (visitor) {
visitor.visit(this);
}
}
Visitor Object:
function Visitor() {}
Visitor.prototype = {
visit: function (employee) {
employee.setSalary(employee.getSalary() * 2);
}
}
`
验证一下:
`const employee = new Employee(‘bruce’, 1000);
const visitor = new Visitor();
employee.accept(visitor);
console.log(employee.getSalary());// 输入:2000
`
场景
- 对象构造中对象对应的类很少扭转,但常常须要在此对象构造上定义新的操作
- 须要对一个对象构造中的对象进行很多不同的并且不相干的操作,而须要防止让这些操作 ” 净化 ” 这些对象的类,也不心愿在减少新操作时批改这些类。
长处
- 合乎繁多职责准则
- 优良的扩展性
- 灵活性
毛病
- 具体元素对访问者颁布细节,违反了迪米特准则
- 违反了依赖倒置准则,依赖了具体类,没有依赖形象。
- 具体元素变更比拟艰难
相干参考资料:
JavaScript 设计模式 es6(23 种)
《前端面试之道》
《JavaScript 设计模式》
《JavaScript 中常见设计模式整顿》