一、设计模式
重中之重!!!:找出变化的地方,使变化的地方与不变的地方分离
1. 单例模式 singleton
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点
实现:用变量标志当前的是否已经为该类创造过实例对象,如果创造过则直接返回该实例,否则创造一个实例返回
let getSingle = (function() { let instance; return function (fn, ...rest) { //fn: 用于创建单例的类 return instance || instance = fn.apply(this, rest); } })()
惰性单例:在需要时才创建实例对象
2.代理模式
当客户不方便直接访问一个对象,或者不满足需要的时候,提供一个替身对象让客户访问,替身对请求进行一些处理后再把请求转交给本体对象
注意:代理对象和本体对象的接口应一致,让客户使用代理对象,这个方便本体和代理使用的替换
保护代理:用于过滤一些请求的代理
虚拟代理:选择在合适的时机处理请求的代理
缓存代理:使用代理暂时缓存远算结果,下次运算先在缓存容器里读取,没有再计算
其他代理:防火墙代理,远程代理,保护代理,智能引用代理,写时复制代理
3.发布订阅模式(PubSub)
定义:又叫观察者模式,定义对象间的一种一对多的依赖关系,当对象改变时,所有依赖它的对象都会得到通知
实现:
- 首先指定好谁充当发布者
- 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
- 发布消息时,发布者遍历这个列表,依次触发存放的订阅者的回调函数
//订阅的通用实现 var event = { clientList: {}, // 缓存列表 listen: function( key, fn ){ //订阅函数, key用来判断发布者发布的事件是否是订阅者所希望收到订阅的 if ( !this.clientList[ key ] ){ this.clientList[ key ] = []; } this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表 }, trigger: function(){ // 发布函数 var key = Array.prototype.shift.call( arguments ), fns = this.clientList[ key ]; if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // arguments 是trigger 时带上的参数 } } }; var installEvent = function( obj ){ for ( var i in event ){ obj[ i ] = event[ i ]; //个人认为应还应判断是否有重名方法 } }; //取消订阅的通用实现 event.remove = function (key, fn) { var fns = this.clientList[key]; if (!fns) { // 如果key 对应的消息没有被人订阅,则直接返回 return false; } if (!fn) { // 如果没有传入具体的回调函数,表示需要取消key 对应消息的所有订阅 fns.length = 0; } else { for (var l = fns.length - 1; l >= 0; l--) { // 反向遍历订阅的回调函数列表, 需要取消订阅的一般靠后,从后面开始遍历性能更好 var _fn = fns[l]; if (_fn === fn) { fns.splice(l, 1); // 删除订阅者的回调函数 } } } };
缺点:
- 创建订阅者本身要消耗一定的时间和内存,特别是订阅的消息一直未触发发布时,但这个订阅者会始终存在于内存中
- 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解
4. 策略模式strategy
个人理解: 将一个对象(事物, 情景)的不同情况都封装为方法,再在不同情况下去调用对应的方法
- 定义:将不同情况的解决办法定义为函数,并用对象封装起来,不同情况调用不同函数
- 目的:使算法的使用和实现分离,内部实现其功能,用户只用关心使用
- 组成:
- 策略类strategy: 封装了解决不同情况的对个算法(函数), 负责计算具体过程
- 环境类context:接收用户的请求,并将请求委托(分发)给对应的策略,因此需要维持对策略类的引用
- 典例:表单验证
<html>
<body>
<form action="http:// xxx.com/register" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName"/ > 请输入密码:<input
type="text" name="password"/ > 请输入手机号码:<input type="text"
name="phoneNumber"/ >
<button>提交</button>
</form>
<script>
/***********************策略对象**************************/
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;
}
},
};
/***********************Validator 类**************************/
var Validator = function () {
this.cache = []; // 将需检查的规则全部缓存,不用考虑顺序
};
// 添加需检查的规则列表
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0, rule; (rule = rules[i++]); ) {
(function (rule) { // 为什么需要闭包?
var strategyAry = rule.strategy.split(":");
var errorMsg = rule.errorMsg;
self.cache.push(function () { //缓存规则检查前的信息加工函数
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rule);
}
};
// 依次调用cache的函数进行检查
Validator.prototype.start = function () {
for (var i = 0, validatorFunc;(validatorFunc = this.cache[i++]);) {
var errorMsg = validatorFunc();
if (errorMsg) {
return errorMsg;
}
}
};
/***********************客户调用代码**************************/
var registerForm = document.getElementById("registerForm");
var validataFunc = function () {
var validator = new Validator();
// 添加检查需检查的表单项
validator.add(registerForm.userName, [
{ strategy: "isNonEmpty", errorMsg: "用户名不能为空" },
{
strategy: "minLength:6",
errorMsg: "用户名长度不能小于 10 位",
},
]);
validator.add(registerForm.password, [
{
strategy: "minLength:6",
errorMsg: "密码长度不能小于 6 位",
},
]);
validator.add(registerForm.phoneNumber, [
{ strategy: "isMobile", errorMsg: "手机号码格式不正确" },
]);
var errorMsg = validator.start();
return errorMsg;
};
//绑定表单验证的事件
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
};
</script>
</body>
</html>
5.迭代器模式
与策略模式的对比:策略模式的各种策略是自己已知的,而迭代器模式所需要元素是未知的,通过迭代后才能确定
定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露对象的内部表示
分类:
- 内部迭代器:规则隐藏在内部,外部不可见,缺点就是迭代规则不可控,交互只在第一次初始调用,
- 外部迭代器:迭代规则可手工控制,使迭代器更灵活,但调用也跟复杂了
- 倒序迭代器:从后面往前遍历的迭代器
6.命令模式
个人理解: 将执行的方法与执行的本体对象分离
没有接收者的智能命令,和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图的不同。
策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标 的不同手段,它们的内部实现是针对“算法”而言的。
而智能命令模式指向的问题域更广,command 对象解决的目标更具发散性
7.组合模式
定义:用小的子对象来构建更大的对象,而这些子对象本身也有更小的孙对象构成
优点:
- 用树形结构表示“部分-整体”的层次结构
- 通过对象的多态性,使用户对单个对象和组合对象的使用具有一致性(既方法名一样)
注意:
- 组合模式不是父子关系,只是组合对象把请求委托给叶子对象(类似职责链模式)
- 组合对象和叶子对象,叶子对象之间都必须是相同的接口,
- 组合对象和子对象为双向映射,一对一的关系,不能给同一个叶子对象两次请求委托
缺点:
- 系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解
- 如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起
8.模板方法模式
个人理解:不变的封装到父类,可变的封装到子类
在 JavaScript 中, 我们很多时候都不需要依样画瓢地去实现一个模版方法模式,高阶函数是更好的选择
9.享元模式
个人理解:一段逻辑本来需要许多重复或相似的对象,但只使用一个对象(带有内部状态)当做模板,每次使用时再包装(传入外部状态)成所需要的对象来达到效果
关键是如何划分内部状态和外部状态:
- 内部状态存储于对象内部
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会改变
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
例子:文件上传
Upload.prototype.delFile = function( id ){ // 文件删除函数
uploadManager.setExternalState( id, this );
if ( this.fileSize < 3000 ){
return this.dom.parentNode.removeChild( this.dom );
}
if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){
return this.dom.parentNode.removeChild( this.dom );
}
}
//实例化上传对象
var UploadFactory = (function(){
var createdFlyWeightObjs = {};
return {
create: function( uploadType){
if ( createdFlyWeightObjs [ uploadType] ){ // 单例模式
return createdFlyWeightObjs [ uploadType];
}
return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
}
}
})();
// 用统一的管理器封装外部状态
var uploadManager = (function(){
var uploadDatabase = {};
return {
add: function( id, uploadType, fileName, fileSize ){
var flyWeightObj = UploadFactory.create( uploadType );
var dom = document.createElement( 'div' );
dom.innerHTML =
'文件名称:'+ fileName +', 文件大小: '+ fileSize +'' +
'';
dom.querySelector( '.delFile' ).onclick = function(){
flyWeightObj.delFile( id );
}
document.body.appendChild( dom );
uploadDatabase[ id ] = {
fileName: fileName,
fileSize: fileSize,
dom: dom
};
return flyWeightObj ;
},
setExternalState: function( id, flyWeightObj ){
var uploadData = uploadDatabase[ id ];
for ( var i in uploadData ){ // 包装(传入外部状态)成所需要的对象
flyWeightObj[ i ] = uploadData[ i ];
}
}
}
})();
// 触发开始上传的函数
var id = 0;
window.startUpload = function( uploadType, files ){
for ( var i = 0, file; file = files[ i++ ]; ){
var uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
}
};
使用场景:
一个程序中使用了大量的相似对象
由于使用了大量对象,造成很大的内存开销
对象的大多数状态都可以变为外部状态
剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象
10.职责链模式
定义:将多个对象连成一条链,若处理不了就将请求向下传递,直到有一个对象能处理,它避免请求对象与多个处理对象之间耦合的局面
优点:
- 解耦了请求发送者和 N 个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可
- 链中的节点对象可以灵活地拆分重组
- 可以手动指定起始节点
缺点
- 需要在最后添加错误处理节点,以防没有成功处理请求的节点
- 可能存在多余的节点并未使用
11.中介者模式
个人理解:让多个对象之间的相互联系变成多个对象只与中介者联系,由中介者统一进行管理,如vuex
优点:
- 解耦对象之间的紧密关系
- 使对象间多对多的关系变成一对多关系
12.装饰者模式
可以配合AOP实现多个函数方法的整体化,又类似适配器模式,可以在外层包装一层函数进行装饰。值得注意的是,它并不会更改原对象
定义: 给对象动态的增加职责(方法)
例子:AOP装饰函数
Function.prototype.before = function( beforefn ){
var __self = this; // 保存原函数的引用
return function(){ // 返回包含了原函数和新函数的"代理"函数
beforefn.apply( this, arguments ); // 执行新函数,可在此处修改参数arguments
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;
}
};
值得注意的是:
- 因为函数通过
Function.prototype.before
或者Function.prototype.after
被装 饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失 - 装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些 影响
代理模式和装饰者模式的区别:在于它们的意图和设计目的。
代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而且代理模式通常只有一层代理本体的引用
装饰者模式的作用就是为对象动态加入行为,用于一开始不能确定对象的全部功能时。而且装饰者模式经常会形成一条长长的装饰链
13.状态模式
定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
应用关键:区别事物上下文(context)内部的状态,事物内部状态的改变往往会带来事物的行为改变,把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
优点:
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换方法
- 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然
- Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响
缺点:
- 会在系统中定义许多状态类,枯燥乏味,而且系统中会因此而增加不少对象
- 由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑
状态模式和策略模式的关系:
相同点:它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行
区别:策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系, 所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法; 而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
14.适配器模式
应用:解决两个软件实体的接口不兼容的问题
装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
二、设计原则
1.单一职责原则(SRP)
定义:一个类应该仅有一个引起它变化的原因, 既一个对象或方法只做一件事情
分离原则:
- 如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们(比如在 ajax 请求的时候,创建 xhr 对象和发送 xhr 请求几乎总是在一起的,那么创建 xhr 对象的职责和发送 xhr 请求的职责就没有必要分开。)
- 另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。
2.最少知识原则(LKP)
定义:一个软件实体应当尽量少的与其他实体发生作用,既减少交互,减少耦合
3.开放-封闭原则(OCP)
定义:软件实体(类,模块,函数等)应该是能扩展,但是不可修改的
三、代码重构建议
- 提炼函数,及时添加注释,不要让函数过长
- 合并重复的条件片段
- 将条件分支语句提炼成函数
- 合理的使用循环,递归
- 使用return提前让条件退出以代替嵌套的条件分支(小技巧:即在面对一个嵌套的 if 分支时,我们可以把外层 if 表达式进行反转,就能转化为多个并级的条件判断语句)
- 传递对象参数以代替过长的参数列表
- 尽量减少参数数量
- 尽量不要用嵌套的三目运算符,该为if
- 合理使用链式调用(原理:方法结束后对象返回自身)
- 分解大型类为多个小类
- 使用return退出多重循环