设计模式主要分为三大类型:创建型模式,结构型模式和行为型模式
创建型设计模式是一类处理对象创建的设计模式,通过某种方式控制对象的创建来避免基本对象创建时可能导致设计上的问题或增加设计上的复杂度。创建型设计模式主要有简单工厂模式,工厂方法模式,抽象工厂模式,建造者模式,原型模式和单例模式。
结构型设计模式关注于如何将类或对象组合成更大、更复杂的结构,以简化设计。主要有外观模式,适配器模式,代理模式,装饰者模式,桥接模式,组合模式和享元模式。
行为型设计模式用于不同对象之间职责划分或算法抽象,行为型设计模式不仅仅涉及类和对象,还涉及类或对象之间的交流模式并加以实现。行为型设计模式主要有模板方法模式,观察者模式,状态模式,策略模式,职责链模式,命令模式,访问者模式,中介者模式,备忘录模式,迭代器模式和解释器模式。
a、constructor属性始终指向创建当前对象的构造函数;
b、我们知道每个函数都有一个默认的属性prototype,而这个prototype的constructor默认指向这个函数。
创建型模式:
简单工厂模式:
又叫静态工厂方式,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
var Factory = funciton(name){
switch(name){
case 'type1':
return new Class1();
case 'type2':
return new Class2();
case 'type3':
return new Class3();
}
}
function createBook(name, time, type){
var o = new Object()
o.name = name
o.time = time
o.type = type
o.getName = function () {
console.log(this.name)
}
return o
}
var book1 = createBook('js book', '2019', 'js')
var book2 = createBook('css book', '2019', 'css')
book1.getName
book2.getName
- 上面两种工厂创建模式,第一种如果有父类的继承,那么它们原型上的方法可以共用;第二种方法不能实现方法的共用,但是可以进行差异化针对性处理。具体看应用场景。
工厂方法模式:
通过对产品类的抽象使其创建业务主要负责用于创建多类产品的实例。
var Factory = function(type, content){
// 安全模式,避免遗漏new
if(this instanceof Factory){
return new this[type](content)
}else {
return new Factory(type, content)
}
}
Factory.prototype = {
Java: function (content) {
// ...
},
JavaScript: function (content) {
// ...
},
UI: function (content) {
this.content = content;
(function (content) {
var div = document.createElement('div');
div.innerText = content;
div.style.border = '1px solid red';
document.getElementById('container').appendChild(div);
})(content)
}
}
var instance1 = Factory( 'UI', '学不动了...' )
- 上述简单工厂模式的Factory方法,如果需求在不断地维护更新,每次都需要更改两个地方。
- 通过工厂方法模式我们可以轻松创建多个类的实例对象,避免了使用者与对象类的耦合,用户不必关心创建该对象的具体类。
- 工厂方法模式本意是将实际创建对象工作推迟到子类当中。这样核心类就成了抽象类。(工厂方法模式看作是一个实例化对象的工厂类)
抽象工厂模式:
通过对类的工厂抽象使其业务用于对产品类簇的创建,而不负责某一类产品的实例。
var VehicleFactory = function (subClass, superClass) {
if(typeof VehicleFactory[superClass] === 'function'){
function F() {};
F.prototype = new VehicleFactory[superClass]();
F.constructor = subClass;
subClass.prototype = new F();
// return new F();
}else {
throw new Error('未创建该抽象类')
}
}
VehicleFactory.Car = function () {
this.type = 'car';
}
VehicleFactory.Car.prototype = {
getPrice: function () {
// 我觉得这里应该用抛出throe比较好,这样能阻止程序继续往下执行,并定位报错地址
return new Error('抽象方法不可使用')
},
getSpeed: function () {
return new Error('抽象方法不可使用')
}
}
VehicleFactory.Bus = function () {
this.type = 'bus';
}
VehicleFactory.Bus.prototype = {
getPrice: function () {
return new Error('抽象方法不可使用')
},
getSpeed: function () {
return new Error('抽象方法不可使用')
}
}
var BMW = function (price, speed) {
this.price = price;
this.speed = speed;
}
VehicleFactory( BMW, 'Car')
// 子类的prototype方法需要在父类VehicleFactory执行之后声明,否则会被覆盖
BMW.prototype.getPrice = function () {
return this.price
}
var car1 = new BMW('666','777')
console.log(car1.getPrice()) // =>666
console.log(car1.getSpeed()) // => Error: 抽象方法不可使用 at BMW.getPrice (VM10818 29.js:102890)
- 定义类簇,指定类的结构。
- 定义一种类,并定义了该类所必备的方法,如果在子类中没有重写这些方法,那么当调用时能找到这些方法便会报错。
建造者模式:
将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。(这些概念真特么看不懂)
// 建造者模式
var Human = function (param) {
this.skill = param && param.skill || '保密'
}
Human.prototype = {
getSkill: function () {
return this.skill
}
}
var Named = function (name) {
var that = this;
(function (name, that) {
that.wholeName = name
})(name, that)
}
var Person = function (name) {
var _person = new Human();
_person.name = new Named(name)
return _person
}
// 两者结果一样
var person1 = new Person('roy')
var person2 = Person('roben')
- 工厂模式面向结果,建造者模式面向过程
原型模式:
用原型实例指向创建对象的类,使用于创建新的对象的类共享原型对象的属性以及方法。
function prototypeExtend () {
var F = function () {},
args = arguments,
i = 0,
len = args.length;
for( ; i<len; i++){
for(var j in args[i]){
F.prototype[j] = args[i][j];
}
}
return new F();
}
var penguin = prototypeExtend({
speed: 20,
swim: function () {
console.log('游泳速度' + this.speed)
}
},{
run: function (speed) {
console.log('奔跑速度' + speed)
}
})
console.log(penguin)
// F: {}
// __proto__:
// run: ƒ run(speed)
// speed: 20
// swim: ƒ swim()
// constructor: ƒ F()
// __proto__: Object
penguin.swim() // =>游泳速度20
penguin.run(666) // =>奔跑速度666
- 原型链最初思想:通过对这些属性方法进行复制来创建(即把它们添加在原型链上,如上述代码的打印结果),避免通过构造函数来创建(考虑到性能消耗,通过构造函数创建,如父类构造函数中创建时存在很多耗时较长的逻辑,或者每次初始化一些重复性的东西)
单例模式:
又被称为单体模式,只允许实例化一次的对象类。有时也可以用一个对象来规划一个命名空间,井井有条地管理对象上的属性和方法。
- 单例模式主要使用场景:
1.单例模式管理常量:
通过闭包实现单例模式管理常量
常量的特性就是不能修改,所以相比声明在全局的常量,用闭包管理管理常量会更加安全
只提供获取常量的方法,所以在闭包里再声明一个特权方法,用于获取常量
(思考:现es6支持import组件引入,而且现在都是使用框架Vue和React组件化开发,这种需要通过单例模式的方式管理常量似乎变得没什么用处,或许在以前没有出现Vue和React时,一个项目需要在一个js文件里从头到尾撸下来,可能就有必要通过这种单例模式来管理常量)
var Conf = (function () {
// 私有变量
var conf = {
JUNIOR: 50,
MIDDLE: 80,
SENIRO: 100,
}
return {
// 专门特权方法,负责读取常量
get: function (type) {
return conf[type] ? conf[type] : null;
}
}
})()
Conf.get('JUNIOR') // => 50
2.惰性单例:
只允许实例一次
var lazySingle = (function () {
var _instance = null
function single () {
// 这里定义私有属性和方法
return {
publicMethod: function () {
},
publicProperty: '1.0'
}
}
return function () {
if(!_instance){
_instance = single();
}
return _instance
}
})()
var instance1 = lazySingle()
var instance2 = lazySingle()
// 只允许实例一次,所以指向同一个对象
console.log(instance1 == instance2) // => true
3.定义命名空间nameSpace(通过一个专有对象进行管理属性和方法的集中管理)
var Roy = {
g: function () {
// ...
},
css: function (id,key,val) {
this.g(id).style[key] = val
}
}
Roy.css('dom', 'color', 'red')
结构型模式:
外观模式:
为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更加容易。在JavaScript中有时也会用于对底层结构兼容性做统一封装来简化用户使用。
- 外观模式,类似于Vue组件封装的思想,把一些复用的内容抽离出来,不管组件内部的业务多复杂,只要暴露出组件的接口
props
参数就行了,这样在一些复用的页面就可以直接引入使用。 - 书中介绍案例,是封装一个方法,用于兼容不同浏览器获取dom操作的方法。
适配器模式:
将一个类(对象)的接口(方法或者属性)转化成另外一个接口,以满足用户需求,使类(对象)之间接口的不兼容问题通过适配器得以解决。
- 应用范围很广:例如适配两个代码库、适配前后端数据等等;
- 参数适配器:例如在es5的函数内,如果是多参数传参,最好是以一个对象为单位进行传参,并做到一定的适配处理,如给参数指定默认值。
// 取代这种传参方式: function doSomething ( name, weight, height, hobby) {}
let param = {
name: 'roy',
weight: 140,
height: 170
}
function doSomething ( param ) {
// es6函数可以直接设置参数默认值
let _param = {
name: 'roy',
weight: 140,
height: 170,
hobby: '保密'
}
for(let i in _param){
param[i] = param[i] || _param[i]
}
// dosomething
console.log('param')
console.log(param)
}
doSomething(param)
- 适配两个代码库,这个在前期技术的选择时,就需要考虑好,如果两个代码库相差很大(如jQuery),需要进行适配器模式进行调整,这个成本就很巨大了。
代理模式:
由于一个对象不能直接引用另一个对象,所以需要通过代理对象在这两个对象之间起到中介作用。
- 如jsonp、img的src进行get请求用于后台统计数据等等
装饰者模式:
在不改变原对象的基础上,通过对其进行包装扩展(添加属性或者方法)使原有对象可以满足用户的更复杂需求。
// 业务场景:在一个旧的form表单交互业务里,新增一个提示输入内容要求的需求,当点击输入框时,新增的提示需求要消失
// 而且除了电话输入提示框,后续还要延用到姓名输入框,密码输入框等等
// 装饰者
var decorator = function (input, fn) {
var input = document.getElementById(input);
if(typeof input.onclick === 'function'){
// 缓存事件原有回调函数
var oldClickFn = input.onclick;
input.onclick = function () {
oldClickFn();
fn()
}
}else {
//事件源未有事件,直接绑定回调函数
input.onclick = fn;
}
}
// 电话输入框功能修饰
decorator('tel_demo_txt', function () {
document.getElementById('tel_demo_txt').style.display = 'none';
})
// 姓名输入框功能修饰
decorator('name_demo_txt', function () {
document.getElementById('name_demo_txt').style.display = 'none';
})
- 装饰者模式,其实就是在一个原有的功能或者业务中,在保证不影响旧功能和业务的前提下,另外封装一个函数(装饰者函数),用于为旧的业务中添加新的需求。
- 觉得跟外观者模式和适配者模式有点像,但是又有区别,
适配者模式主要是兼容并保留旧的需求,不新增新需求;
外观者模式是面对一些可以复用或者兼容性的业务需求,并从而封装一个函数;
而装饰者模式则是在旧业务的基础上,添加新需求。
桥接模式:
在系统沿着多个维度变化的同时,又不增加其复杂度并已达到解耦。
// 运动单元
function Speed (x,y) {
this.x = x;
this.y = y;
}
Speed.prototype.run = function () {
console.log('动起来')
}
// 说话单元
function Speak (wd) {
this.word = wd;
}
Speak.prototype.say = function () {
console.log('书写字体')
}
// 变形单元
function Shape (sp) {
this.shape = wd;
}
Shape.prototype.change = function () {
console.log('改变形状')
}
// 创建人物
var People = function ( x, y, f) {
this.speed = new Speed( x, y )
this.font = new Speak( f )
}
People.prototype.init = function () {
this.speed.run()
this.font.speak()
}
// 创建精灵
var Spirite = function ( x, y, sp) {
this.speed = new Speed( x, y )
this.shape = new Shape( sp )
}
Spirite.prototype.init = function () {
this.speed.run()
this.shape.change()
}
var p = new People(10,12,16)
p.init()
- 桥接模式最主要的特点即是将实现层(如元素绑定的事件)与抽象层(如修饰页面的UI逻辑)解耦分离,使两部分可以独立化。
- 避免需求的改变造成对象内部的修改,体现了面向对象对拓展的开放及对修改的关闭原则
- 在以后使用canvas进行动画开发时,可以考虑这种模式。
组合模式:
又称部分-整体模式,将对象组合成树形结构以表示“部分整体”的层级结构。组合模式使得用户对单个对象和组合对象的使用具有一致性
- 一开始觉得这种设计模式,有点类似于创建型设计模式的建造者模式,但是仔细看还是有区别的,建造者模式最终的结果是通过组合的方式生成一个对象,而组合模式面对的一个业务需求和生成需求的过程。
- 组合模式主要针对一些树型结构的业务需求,例如form表单的输入框组合、新闻页面的列表结构等,通过父类继承的方法,进行分析抽离出可以快速组合使用的功能。
享元模式:
运用共享技术有效地支持大量的细粒度的对象,避免对象间拥有相同内容造成多余的开销。
行为型模式:
模板方法模式:
观察者模式:
存在三者关系(被观察者、观察者、订阅者)
被观察者向观察者通知消息,观察者再把收到的消息发布到订阅者
因此基本观察者对象包括四个组成:
- 注册信息
- 取消注册
- 发布消息
- 消息容器
var Observer = (function () {
// 将观察者放在闭包中,当页面加载就立即执行
var _message = {};
return {
// 注册信息接口
regist: function (type, fn) {
// 如果此消息不存在则应该创建一个该消息类型
if(typeof _message[type === 'undefined']){
// 将动作推入到该消息对应的动作执行队列中
_message[type] = [fn]
}else {
// 将动作推入到该消息对应的动作执行队列中
_message[type].push(fn)
}
},
// 发布信息接口
fire: function (type, args) {
// 如果该消息没有被注册,则返回
if(!_message[type])
return;
// 定义消息信息
var events = {
type: type,
args: args || {}
},
i = 0,
len = _message[type].length;
for(; i < len; i++){
// 依次执行注册的消息对应的动作序列
_message[type][i].call(this, events)
}
},
// 移除注册信息接口
remove: function () {
// 如果消息动作队列存在
if(_message[type] instanceof Array){
// 从最后一个消息动作遍历
var i = _message[type].length - 1
for(; i>=0; i--){
// 如果存在该动作则在消息动作序列中移除相应动作
_message[type][i] === fn && _message[type].splice(i,1);
}
}
}
}})();
// 模拟地面接收站
(function () {
function focus(event){
console.log('正在实时监控飞机状态...')
console.log(`飞机${event.args.height}${event.args.status}`)
}
Observer.regist('airplane', focus);
})();
// 模拟飞机飞行, 定时通知观察者对象
(function () {
var height = 1000,
i = 0;
setInterval(()=>{
i ++
Observer.fire('airplane',{
status: '正常飞行',
height: `海拔高度${height*i}Km`
})
},10000)
})();
上述订阅者和发布者都是在一个闭包里面,由此,通过观察者设计模式,解决了主题对象和观察者之间功能的耦合,避免了我们需要在旧的功能模块上去改代码。
观察者模式最主要的作用是解决类或对象之间的耦合,解耦两个相互依赖的对象,使其依赖于观察者的信息机制。
状态模式:
当一个对象内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对像。
// 又一次体现了闭包的重要性
var MarryState = function () {
var _currentState = {},
state = {
jump: function () {
console.log('jump')
},
move: function () {
console.log('move')
},
shoot: function () {
console.log('shoot')
},
squat: function () {
console.log('squat')
}
};
var Action = {
// 改变状态
changeState: function () {
_currentState = {};
var arg = arguments;
if(arg.length > 0){
for(var i=0; i<arg.length; i++){
_currentState[arg[i]] = true
}
}
return this;
},
// 执行动作
gose: function () {
for(let i in _currentState){
state[i] && state[i]()
}
return this;
}
}
return {
change: Action.changeState,
gose: Action.gose
}
}
MarryState().change('jump','shoot').gose()
var marry = new MarryState()
marry.change('jump','shoot')
.gose() // jump shoot
.change('squat')
.gose() // squat
.gose() // squat
有时候状态很多,而且有些状态是可以组合的,如果用单纯的if和switch进行控制判断,耦合性会很强,不利于维护。
将不同的判断结果封装在对象内,然后该状态对象返回一个可被调用的接口方法,用于调用状态对象内部某种方法。
状态模式既是解决程序中臃肿的分支判断语句问题,将每个分支转换为一种状态独立出来(解决条件分支之间的耦合问题),方便每种状态的管理又不至于每次执行时遍历所有分支。
在程序中到底产出哪种行为结果,决定于选择哪种状态,而选择何种状态又是在程序运行时决定的。
策略模式:
将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定独立性,不会随客户端变化而变化。
var PriceDiscountType = (function () {
var discountType = {
// 100 返 30
'return30': function (price) {
// parseInt可以通过~~、|等运算符替换
return +price + parseInt(price/100)*30
},
'return50': function (price) {
return +price + parseInt(price/100)*50
},
// 打5折
'percent50': function (price) {
return price*100*50/10000
}
}
return function (type, price) {
return discountType[type] && discountType[type](price)
}
})()
var price1 = PriceDiscountType('return50',100.63)
var price2 = PriceDiscountType('percent50',100.63)
console.log('price')
console.log(price1) // 150.63
console.log(price2) // 50.315
- 策略模式和状态模式,初次学习的时候,感觉二者很近似,但是仔细分析下,二者还是有区别的。状态模式,其上下文对象(context)是其本身,就如上述的玛丽例子,每次状态的改变,都是改变本身的状态,从而改变状态下对应的功能(方法); 而策略模式它的上下文对象是使用了策略模式的对象,其行为由其包含的具体策略所决定。
职责链模式:
解决请求的发送者与请求的接受者之间的耦合,通过职责链上的多个对象对分解请求流程,实现请求在多个对象之间的传递,知道最后一个对象完成请求的处理。
命令模式:
将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端的实现参数化。
<div id='title'></div>
<div id='product'></div>
var viewCommand = (function () {
var tpl = {
// 展示图片结构模板
product: [
'<div>',
'<img src="{#src#}"/>',
'<p>{#text#}</p>',
'</div>'
].join(''),
title: [
'<div class="main">',
'<h2>{#title#}</h2>',
'<p>{#tips#}</p>',
'</div>'
].join('')
},
html = '';
function formateString (str, obj) {
return str.replace(/\{#(\w+)#\}/g, function (match, key) {
return obj[key]
})
}
// 方法集合
var Action = {
// 创建方法
create: function (data, view) {
if(data.length){
for(var i = 0, len = data.length; i<len; i++){
html += formateString(tpl[view], data[i]);
}
}else {
html += formateString(tpl[view], data);
}
},
// 展示方法
display: function (container, data, view) {
if(data){
this.create(data, view);
}
document.getElementById(container).innerHTML = html;
html = '';
}
}
return function excute(msg) {
msg.param = Object.prototype.toString.call(msg.param) === '[object Array]' ?
msg.param : [msg.param];
Action[msg.command].apply(Action, msg.param)
}
})();
// 测试数据
var producData = [
{
src: 'command/02.jpg',
text: '绽放的菊花02'
},
{
src: 'command/03.jpg',
text: '阳光下的温馨03'
}],
titleData = {
title: '大标题',
tips: '副标题'
};
viewCommand({
command: 'display',
param: ['title', titleData, 'title']
})
viewCommand({
command: 'create',
param: [{
src: 'command/01.jpg',
text: '绽放的太阳花01'
},'product']
})
viewCommand({
command: 'display',
param: ['product', producData, 'product']
})
- 命令模式将请求模块与实现模块进行解耦
- 命令模式是将执行的命令封装,解决命令的发起者与命令的执行者之间的耦合。每一条命令实质上是一个操作。命令的使用者不必要了解命令的执行者(命令对象)的命令接口是如何实现的,命令是如何接受的、命令是如何执行的。所有的命令都被存储在命令对象中。
- 命令模式的优点自然是解决命令使用者之间的耦合。新的命令很容易加入到命令系统中,供使用者使用。
- 命令的使用具有一致性,多数的命令在一定程度上是简化操作方法的使用的。
访问者模式:
针对于对象结构中的元素,定义在不改变对象的前提下访问结构中元素的新方法。
// 低版本IE的事件兼容问题
var bindEvent = function (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;
}
}
var demo = document.getElementById('demo');
bindEvent(demo, 'click', function () {
this.style.red = 'red' // 低版本IE,这里的this指向全局对象winodw
})
function bindIEEvent (dom, type, fn, data) {
var data = data || {};
dom.attachment('on'+type, function (e) {
fn.call(dom, e, data); // 实现的核心是调用了一次call方法。
// 这里让我们的事件源元素对象在我们的事件回调函数中的作用域中运行,那么我们在回调函数访问的this当然指向我们的事件源对象了
})
}
// 创建访问器(通过访问者模式(利用Array的原生方法),创建类数组对象)
var Visitor=(function(){
return {
//截取方法
splice:function(){
var args=Array.prototype.splice.call(arguments,1);
return Array.prototype.splice.apply(arguments[0],args);
},
//追加数据方法
push:function(){
var len=arguments[0].length || 0;
var args= this.splice(arguments,1);
arguments[0].length=len+arguments.length-1; // 修改length值
return Array.prototype.push.apply(arguments[0],args);
},
//删除最后一次添加成员
pop:function(){
return Array.prototype.pop.apply(arguments[0]);
}
}
})();
var a=new Object();
Visitor.push(a,4,5,6);
Visitor.pop(a);
Visitor.splice(a,2);
console.log(a);
- 通过call和apply让某个对象在其他作用域中运行
- JavaScript的源生对象构造器就设计成一个访问者
中介者模式:
通过中介者对象封装一系列对象之间的交互,是对象之间不再相互引用,降低他们之间的耦合。有时中介者对象也可以改变对象之间的交互。
//中介者对象
var Mediator=function(){
//消息对象
var _msg={};
return{
//订阅方法
register:function(type,action){
//如果存在该消息
if(_msg[type]){
//存入回调函数
_msg[type].push(action);
}else{
//不存在我们建立消息容器
_msg[type]=[];
//存入新消息回调函数
_msg[type].push(action);
}
},
//发布方法
send:function(type){
//如果该消息被订阅
if(_msg[type]){
//遍历存储的消息回调函数
for (var i=0;i<_msg[type].length;i++) {
_msg[type][i]&&_msg[type][i]();
}
}
}
}
}();
Mediator.register('demo',function () {
console.log('first')
})
Mediator.register('demo',function () {
console.log('second')
})
Mediator.send('demo')
// first
// second
- 参考例子
- 与观察者模式相比,两者都是通过消息传递实现对象间或模块间的解耦。观察者模式中的订阅者是双向的,而中介者模式中的订阅者是单向的,只能是消息的订阅者。而消息统一由中介者对象发布,所有的订阅者对象间接被中介者管理。
备忘录模式:
在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部状态以便日后对象使用或者对象恢复到以前的某个状态。
var Page = (function (){
// 信息缓存对象
var cache = {};
function showPage () {
};
return function (page, fn) {
if(cache[page]){
showPage(page, cache[page]);
// 执行成功回调函数
fn && fn()
}else {
$.post('./data/getNumber.php',{
page
}, function (res) {
showPage(page, res.data);
cache[page] = res.data;
// 执行成功回调函数
fn && fn()
})
}
}
})()
- 如用于分页数据的缓存,而不用每次都重复请求同样的数据
- 备忘录模式主要的任务是对现有的数据或状态缓存,为将来某个时刻使用或恢复做准备。
迭代器模式:
在不暴露对象内部结构的同时,可以顺序地访问聚合对象内部的元素。
解释器模式:
对于一种语言,给出其文法表示,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。