《javaScript设计模式与开发实践》笔记
内容
- 各种模式的定义和作用
- 模式的优缺点
- 模式在实际项目中的用途
- 模式之间的关系
第一章 原型模式
js对象通过原型链来实现继承和查找对象属性
第二章 this, bind, call, apply;
- this指向,默认window,普通函数、箭头函数、对象方法、原型方法中使用
- call和apply区别,参数不同,应用场景
-
- 修正this,指向
- 借用该类型本没有的方法,如
[].push.call(obj, xx)
第三章 闭包、高阶函数
- 闭包概念、使用场景
- 高阶函数特征,参数、返回值为函数 应用广泛
-
- 异步回调、数组排序、遍历等
- 返回值为函数,即函数操作可持续进行,柯里化、uncurry
- 分时函数,1000个好友分批创建dom节点,每批加8个
var timeChunk = function( ary, fn, count ){
var obj,
t;
var len = ary.length;
var start = function(){
for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
var obj = ary.shift();
fn( obj );
}
};
return function(){
t = setInterval(function(){
if ( ary.length === 0 ){ // 如果全部节点都已经被创建好
return clearInterval( t );
}
start();
}, 200 ); // 分批执行的时间间隔,也可以用参数的形式传入
};
};
- 惰性加载函数,实例:浏览器嗅探,只执行一次条件语句,且不调用函数时不执行
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
addEvent = function( elem, type, handler ){
elem.addEventListener( type, handler, false );
}
}else if ( window.attachEvent ){
addEvent = function( elem, type, handler ){
elem.attachEvent( 'on' + type, handler );
}
}
addEvent( elem, type, handler );
};
第4章 单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点
作用:有些对象很有用但在项目中只需要出现一个,就可以使用单例模式创建
实现:传统面向对象语言的单例模式,通过new来创造实例对象,通过 getInstance 来获取唯一的对象;在js中可以通过闭包和高阶函数实现单例对象
用途:线程池、框架的全局对象、window、全局缓存等
注意点:避免变量污染,可以使用使用命名空间或闭包
var ctx;
if(!ctx){
ctx = xx
}
将它封装在闭包中,就实现惰性的单例方法,只在需要的时候创建一次。
再通过分离fn和单例方法,来实现通用的惰性单例
function getSingle(fn){
let instance;
return function (){
return instance || (instance = fn.apply(this, arguments))
}
}
第五章 策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。
目的:将算法的使用和算法的实现分离开。策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值。
涉及的原则:开放-封闭原则、复用原则、最少知识原则
优点:将策略算法与Context逻辑分离;利用多态、委托、组合等思想和技术,有效避免if/else分支;可复用、易扩展
缺点:违反最少知识原则,必须了解所有strategy
第六章 代理模式
定义:为一个对象提供一个代用品或占位符,以便控制对它的访问
代理模式的关键是, 当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身 对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之 后,再把请求转交给本体对象
涉及的原则:依赖倒置原则,单一职责原则,开放-封闭原则
扩展
- 保护代理:保护代理用于控制不同权限的对象对目标对象的访问
- 虚拟代理 : 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建
什么时候用?当真正发现不方便直接访问某个对象的时候,再编写代理也不迟
第七章 迭代器模式
定义:迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
作用:迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
优点:
- 支持不同方式遍历一个聚合对象;
- 迭代器简化了聚合类;
- 同一个聚合上可以有多个遍历;
- 增加新的聚合类和迭代器类方便
缺点:新的聚合类需要对应增加新的迭代器类,类的个数成对增加,系统复杂度一定程度上增加
使用:内部迭代器(遍历),外部迭代器(提供next和结束的迭代),倒序迭代器、中止迭代器...
第八章 发布订阅模式
定义:发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状
态发生改变时,所有依赖于它的对象都将得到通知。
作用:可以广泛应用于异步编程;可以取代对象之间硬编码的通知机制。
应用:公众号订阅、餐厅叫号等
优点:时间上的解耦,对象间的解耦
缺点:需要消耗一定的时间和内存,可能会出现订阅以后一直未发布的情况,但订阅对象一直在内存中
var event = {
clientList: [],
listen: function( key, fn ){
if ( !this.clientList[ key ] ){
this.clientList[ key ] = [];
}
this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表
},
trigger: function(){
var key = Array.prototype.shift.call( arguments ), // (1);
fns = this.clientList[ key ];
if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
return false;
}
for( var i = 0, fn; fn = fns[ i++ ]; ){
fn.apply( this, arguments ); // (2) // arguments 是 trigger 时带上的参数
}
}
};
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
};
第九章 命令模式
定义:命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令
作用:请求对象和调用对象解耦;命令对象方便传递;支持撤销和排队
优点:降低耦合、新命令易添加
缺点:额外维护命令对象,可能有过多的命令类
项目中的应用:操作栏命令、电视机命令
其他应用: 宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
智能命令 vs 策略模式: 没有接收者的智能命令,退化到和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图的不同。策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标的不同手段,它们的内部实现是针对“算法”而言的。而智能命令模式指向的问题域更广,command 对象解决的目标更具发散性。命令模式还可以完成撤销、排队等功能。
第十章 组合模式
定义:组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成。
思想:事物是由相似的子事物构成
应用场景
- 表示对象的部分-整体层次结构。
- 利用多态性统一对待树中的所有对象
注意点:
- 组合模式不是父子关系
- 对叶节点对象保持一致性
- 双向映射关系不适合用组合模式,比如架构师属于不同部门,用组合模式可能收到多份节日费
缺点:
- 太多的节点带来的性能问题,可借助职责链模式处理
- 与普通树对象难以区分
项目中的应用:组件成组功能
第十一章 模版方法模式
定义 模版模式是基于继承来实现的一种模式
组成 模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
解决:避免子类重复出现相同方法
思想:封装变化提高系统可扩展性
项目中的应用:com类,各种框架模板
通过钩子函数可以实现更多的变化,如某一步骤是否调用
第十二章 享元模式
定义:享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。
如何区分内部状态和外部状态?
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
目标:尽量减少共享对象的数量
什么时候用:
- 对象的大多数状态都可以变为外部状态。 6
- 一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
项目应用:文件上传创建了大量的上传对象
优缺点:优点是优化了性能,避免资源浪费;缺点是代码复杂度提高,需要分离和维护内部状态与外部状态
第十三章 职责链模式
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
优点:
- 请求发送者只需要知道链中的第 一个节点,从而弱化了发送者和一组接收者之间的强联系
- 使用了职责链模式之后,链中的节点对象可以灵活地拆分重组
- 可以手动指定起始节点,请求并不是非得从链中的第一个 节点开始传递。
缺点:
- 我们不能保证某个请求一定会被链中的节点处理。
- 职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分 节点并没有起到实质性的作用
使用场景:作用域链、原型链、事件冒泡等
第十四章 中介者模式
作用:解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。
优点:对象解耦,符合最少知识原则
缺点:加入了中介对象,对象关系越复杂,中介对象越难维护
什么时候用:对象之间的复杂耦合导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线
建议:中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。
项目应用:商品页,商品参数与库存关系
第十五章 装饰者模式
定义:给对象动态地增加职责的方式称为装饰者模式。
作用:装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
对比继承:装饰者模式更灵活,“即插即用”,不会影响原函数
对比代理模式:主要是意图和设计目的不同。代理的关系是本体与代理,本体可以拒绝访问者;装饰者更像是给对象赋予功能
使用场景:需要职责分离,为方法提供可插拔的功能。框架中使用的多,比如表单的 validate
优点:符合开放-封闭原则、职责分离原则
缺点:装饰链可能会比较长
思考:
继承: A 对象有init 方法和fire方法,init给初始化一些属性;B想拥有属性,继承了A对象,并重写fire方法
装饰者:A还是A,只是想给fire加点功能,通过改写Function原型,用before和after钩子可以轻松给fire加功能而且不用改写fire内部
let a={
init: function(){},
fire: function(){}
}
Function.prototype.before = function(beforeFn){
var _self = this;
return function (){
beforeFn.apply(this, arguments)
return _self.apply(this, arguments)
}
}
a.fire.before(function(){
//添加功能
})
第十六章 状态模式
定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
结构:上下文,状态类,状态类中封装行为,且每个状态类都实现了统一接口
优点:
- 有状态类,容易增加新的状态
- 避免Context无限膨胀,去掉了条件语句
- 对象代替字符串记录状态,更清晰
- Context请求与状态分离,方便更改
缺点:封装了很多状态类,状态类太多时逻辑容易分散,无法在一个地方看出有哪些状态
对比策略模式:它们类图很相似,都有Context上下文,状态或策略类,上下文将请求交给状态类执行。
意图不同,策略类之间平等且平行,客户需要知道它们的作用
状态类之间切换,且行为早已封装好,行为改变发生在状态内部,客户不需要知道细节。
应用场景:文件上传状态切换、音乐器播放状态、酒店智能开关状态
第十七章 适配器模式
定义:像现实中的电源适配器一样对不同的对象接口进行适配
作用:适配器模式的作用是解决两个软件实体间的接口不兼容的问题
对比其他模式:
- 适配器模式解决接口不匹配的问题,不考虑接口如何实现,也不考虑将来如何演化,只包装一次且不改变原有接口
- 装饰者模式主要为给对象增加功能,可能有很长的装饰链,适配器只会有一层
- 代理模式为控制对对象的访问,通常也只包装一次
应用:本地数据与第三方接口数据格式不一致,对第三方接口数据进行适配
第十八章 单一职责原则
定义:一个方法处理一件事
有哪些模式应用:代理模式、迭代器模式等
优点:降低类或对象复杂度,方便测试、复用,修改时不影响其他对象
缺点:增大代码复杂度
应用建议:方便性与稳定性取舍问题,根据具体情况判定,不一定非得遵守该原则;什么情况下不需要分离?
- 随需求变化,两个职责都同时变化,就不需要分离,比如xhr创建请求对象和发送xhr对象
- 两个职责耦合在一起但没有变化的征兆
第十九章 最少知识原则
定义:实体之间尽可能少的相互作用。在应用中,可以引入第三方来管理实体之间的联系
有哪些模式应用:中介者模式、命令模式
优缺点同上
第二十章 开放-封闭原则
定义:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。需要加功能的时候不改动源码
实现:
- 利用对象多态性消除条件分支,示例:鸭子类
- 封装变化,hook钩子、回调函数
设计模式:发布订阅、模板方法、策略模式、代理模式、职责链模式
注意点:
- 尽可能挑出最容易变化的地方,构建抽象来封闭变化
- 需要修改时,尽量修改容易修改的地方
- 接受一次愚弄,可以在需要封闭时再去做
第二十一章 接口和面向接口编程
什么是接口?
- 字面意义上的api接口,供外部使用,不需关注内部实现
- 静态语言提供的关键字 interface,为编写类提供一种契约,方便编译器检查
- 面向接口编程的接口
向上转型的方式:
- 抽象类
示例:鸭子类型中,鸭子和鸡类继承往上一层的抽象类 animal,实现了契约和多态,在编写过程中能被类型检查到
面向接口编程在过程上看更像“面向超类型编程”,对象的具体类型隐藏在超类型背后,对象就可以相互替换使用。
- interface
interface实际上也是继承的一种方式,叫接口继承
第二十二章 代码重构
熟悉设计模式和代码重构工作总是很难分开的,针对代码重构提出些建议
- 提炼函数: 大型需要多段注释才能函数需要提炼出来,起个好的名字本身可以代替注释
- 合并重复条件分支语句:每个条件分支都出现的代码片段,可以提出来
- 条件分支提炼成函数:例如
if(/^[a-zA-z]+://[^\s]*$/.test(val))
可以将语句封装为工具函数 isUrl,if(isUrl)
- 合理使用循环:示例 嵌套try catcch
- 提前让函数退出代替嵌套条件分支:错误处理置前
- 对象参数代替过长参数:传递的参数过多就用对象代替,就不用在意传递顺序
- 少用多层三目运算:可读性并不高的长三目运算,还是老老实实用条件语句吧
- 尽量减少参数数量:(w, h, area) 面积能用宽高算就别传了
- 合理使用链式调用:易变动的方法链式调用难调试
- 分解大型类:示例 游戏类又管流程又管攻击类型,干脆把攻击方式提出来作为攻击类
- return退出大型循环:多层循环,用flag标记满足条件后退出,可以直接在满足条件时return
转载自:https://juejin.cn/post/7190261077148434489