设计模式

设计模式

  • 设计模式是我们在 解决问题的时候针对特定问题给出的简洁而优化的处理方案

设计模式分为三种类型,共23种。创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式(责任链模式)、访问者模式、发布-订阅模式。

程序设计主要原则:
开闭原则,即开放扩展,关闭修改
单一职责原则,值一个类或者模块应该有且只有一个改变的原因
高内聚低耦合,内聚性又称块内练习(模块内各元素联系的紧密性),耦合性也称块内联系(模块之间的独立性)

工厂模式

工厂模式是用来创建对象的一种最常用的设计模式。
在实际开发中,工厂模式主要用于复杂的对象构建、生成多个不同的实例对象等场景。


function factory(engine,speed,color) {    
	var car = {}; //原料    
	car.engine = engine; //加工    
	car.speed = speed; //加工    
	car.color = color; //加工    
	car.drive = function () {        
		console.log('最高时速为:' + car.speed);    
	}    
	return car; //出厂
}
var car1 = factory('v4','140km/h','blue');
var car2 = factory('v6','180km/h','red');


var factory = (function (){    
	var car = {        
		carA: function (){            
			this.type = '高配版';            
			this.engine = 'v8引擎';            
			this.speed = '最高时速320km/h';        
		},        
		carB: function (){            
			this.type = '中配版';            
			this.engine = 'v6引擎';            
			this.speed = '最高时速220km/h';        
		},        
		carC: function (){            
			this.type = '低配版';            
			this.engine = 'v4引擎';            
			this.speed = '最高时速180km/h';        
		}    
	}    
	return function (config){        
		return new car[config]();    
	}
})();
var car1 = factory('carA');
var car2 = factory('carB');
console.log(car1.type);
console.log(car2.type);

单例模式

  • 什么是单例模式呢?
  • 我们都知道,构造函数可以创造一个对象
  • 我们 new 很多次构造函数就能得到很多的对象
  • 单例模式: 就是使用构造函数实例化的时候,不管实例化多少回,都是同一个对象
    • 也就是一个构造函数一生只能 new 出一个对象
  • 也就是说,当我们使用构造函数,每一次 new 出来的对象 属性/功能/方法 完全一样 的时候,我们把他设计成单例模式

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

应用场景:对象仅需要创建一个的时候
1.windows的任务管理器
2.多线程的线程池设计
3.全局缓存
4.浏览器的window对象
5.页面中的登录弹窗,无论点击多少次,弹窗只会被创建一次

核心代码

  • 单例模式的核心代码很简单

  • 其实就是判断一下,他曾经有没有 new 出来过对象

  • 如果有,就还继续使用之前的那个对象,如果没有,那么就给你 new 一个

    // 准备一个构造函数
    // 将来要 new 的
    function Person() {}
    
    // 准备一个单例模式函数
    // 这个单例模式函数要把 Person 做成一个单例模式
    // 将来再想要 new Person 的时候只要执行这个 singleton 函数就可以了
    function singleton () {
      let instance
      
      if (!instance) { // 如果 instance 没有内容
        // 来到这里,证明 instance 没有内容
        // 给他赋值为 new Person
          instance = new Person()
      }
      
      // 返回的永远都是第一次 new Person 的实例
      // 也就是永远都是一个实例
      return instance
    }
    
    const p1 = singleton()
    const p2 = singleton()
    console.log(p1 === p2) // true

应用

  • 我们就用这个核心代码简单书写一个 demo

    // 这个构造函数的功能就是创建一个 div,添加到页面中
    function CreateDiv() {
        this.div = document.createElement('div')
        document.body.appendChild(this.div)
    }
    
    CreateDiv.prototype.init = function (text) {
        this.div.innerHTML = text
    }
    
    // 准备把这个 CreateDiv 做成单例模式
    // 让 singleton 成为一个闭包函数
    const singleton = (function () {
    
        let instance
    
        return function (text) {
            if (!instance) {
                instance = new CreateDiv()
            }
            instance.init(text)
            return instance
        }
    })()
    
    singleton('hello') // 第一次的时候,页面中会出现一个新的 div ,内容是 hello
    singleton('world') // 第二次的时候,不会出现新的 div,而是原先的 div 内容变成了 world

代理模式

代理模式是为一个对象提供一个代理,以便控制对它的访问。

代理模式分成两个部分,一个部分是本体,即为你想要实现的功能;另一部分为代理,代理可以代替本体做一些处理。
在这里插入图片描述

不用代理模式:


var Gift = function (n) {    
	this.name = n;
};
var boy = {    
	sendGift: function (target) {        
		var flower = new Gift('花');        
		target.receiveGift(flower);    
	}
}
var girl = {    
	receiveGift: function (gift) {        
		console.log('收到礼物:' + gift.name);    
	}
}
boy.sendGift(girl);

使用代理模式:


var Gift = function (n) {    
	this.name = n;
};
var boy = {    
	sendGift: function (target) {        
		var flower = new Gift('花');        
		target.receiveGift(flower);    
	}
}
var guimi = {    
	receiveGift: function (gift) {        
		girl.receiveGift(gift);    	
	}
}
var girl = {    
	receiveGift: function (gift) {        
		console.log('收到礼物:' + gift.name);    
	}
}
boy.sendGift(guimi);

在当前场景中,自己送花与找人代送没什么本质区别,代理模式确实没什么用!
我们来改变一下场景设置,假设 girl 收到花的时候,心情好时的成功几率是60%,心情不好时的成功几率为2.222%…


var Gift = function (n) {    
	this.name = n;
};
var boy = {    
	sendGift: function (target) {        
		var flower = new Gift('花');        
		target.receiveGift(flower);    
	}
}
var guimi = { // 代理可以代替本体做一些处理    
	receiveGift: function (gift) {       
	 // 假设 2 秒之后 girl 的心情变好        
	 setTimeout(function () {            
	 	girl.receiveGift(gift);        
	 },2000);    
	}
}
var girl = {    
	receiveGift: function (gift) {        
		console.log('收到礼物:' + gift.name);    
	}
}
boy.sendGift(guimi);

普通预加载图片


var myImage = (function () {        
	var imgNode = document.createElement('img');        
	document.getElementById('loadImg').appendChild(imgNode);        
	var img = new Image();        
	img.onload = function () {            
		imgNode.src = img.src;        
	}        
	return {            
		setSrc: function (src) {                
			imgNode.src = 'loading.gif';                
			img.src = src;            
		}        
	}
})();
myImage.setSrc('http://pic39.photophoto.cn/20160411/1155116845138548_b.jpg');

上段代码中的 myImage 对象除了负责给 imgNode 设置 src 外,还要负责预加载图片,违背单一职责原则。

单一职责原则指的是,就一个类(也包括对象和函数等)而言,应该仅有一个引起它变化的原因。

假如进入5G时代不再需要预加载,我们不得不改动 MyImage 对象!

代理模式预加载图片


var imageModule = (function (){// 本体对象  
	var showimg = new Image();  
	return {    
		setSrc: function (dom,src){      
			showimg.src = src;      
			dom.appendChild(showimg);    
		}  
	}
})();
var proxyModule = (function (){// 代理对象  
	var loadimg = new Image();  
	var showDom;  
	loadimg.onload = function (){    
		imageModule.setSrc(showDom,this.src);  
	}  
	return {    
		setSrc: function (dom,src){      
			showDom = dom;      
			loadimg.src = src;      
			imageModule.setSrc(dom,'./loading.gif');    
			}  
		}
	})();
	proxyModule.setSrc(box,'http://pic39.photophoto.cn/20160411/1155116845138548_b.jpg');

通过proxyModule间接地访问imageModule,代理对象proxyModule控制了客户对imageModule的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把showimg的src设置为一张本地的loading图片。

策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

一个基于策略模式的程序至少由两部分组成:

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程;

第二个部分是环境类Context,Context接受客户的请求,随后把请求委托给某个策略类。‘算法’:各种业务规则

demo1:计算奖金,基于传统面向对象语言的模仿


var performanceA = function () {}; // 策略类  
performanceA.prototype.calculate = function (salary) {    
	return salary * 8;
}
var performanceB = function () {}; // 策略类
performanceB.prototype.calculate = function (salary) {    
	return salary * 5;
}
var performanceC = function () {}; // 策略类
performanceC.prototype.calculate = function (salary) {    
	return salary * 3;
}
var Bonus = function () { // 奖金类(环境类)    
	this.salary = null; // 原始工资    
	this.strategy = null; // 绩效等级对应的策略对象
}
Bonus.prototype.setSalary = function (salary) {    
	this.salary = salary; // 设置原始工资
}
Bonus.prototype.setStrategy = function (strategy) {    
	this.strategy = strategy; // 设置绩效等级对应的策略对象
}
Bonus.prototype.getBonus = function () { // 取得奖金数额    
	// 把计算奖金的操作委托给对应的策略对象    
	return this.strategy.calculate(this.salary);
}
var bonus = new Bonus(); // 奖金类实例
bonus.setSalary(10000); // 设置原始工资
bonus.setStrategy(new performanceA()); //设置策略对象
console.log(bonus.getBonus()); // 80000
bonus.setStrategy(new performanceB()); //设置策略对象
console.log(bonus.getBonus()); // 50000

demo2:JavaScript版本的策略模式


var strategies = { //策略对象,封装算法    
	'A': function (salary) {        
		return salary * 8;    
	},    
	'B': function (salary) {        
		return salary * 5;    
	},    
	'C': function (salary) {        
		return salary * 3;    
	}
}
var getBonus = function (level, salary) { //环境类,接受客户的请求    
	// 把计算奖金的操作委托给对应的策略对象    
	return strategies[level](salary);
}
console.log(getBonus('A', 10000)); //80000
console.log(getBonus('B', 10000)); //50000

发布-订阅模式

发布-订阅模式(观察者模式),它定义对象间的一种 一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布-订阅模式。最常用且最简单的发布-订阅模式:DOM事件


document.body.addEventListener('click', function () {    
	alert('消息');
}, false);

订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息

// 发布订阅模式,是一种处理一对多的关系
function Observe(){  
	this.cacheList = {};// 缓存列表  
	// {  
	//   'houseTypeA': [fn1,fn2,fn3],  
	//   'houseTypeB': [fn1,fn2,fn3,fn4],  
	//   'houseTypeC': [fn1,fn2]  
	// }
}
Observe.prototype = {  
	constructor: Observe,  
	// 发布:取出对应的消息类型,执行所有回调函数传入发布的消息  
	publish: function (type,message){    
		if (!this.cacheList[type]){      
			return;    
		}    
		this.cacheList[type].forEach(function (cb){      
			cb(message);    
		});  
	},  
	// 订阅:往某个消息类型中添加回调函数  
	subscribe: function (type,callback){    
		// 判断对象是否有type此属性    
		if (!this.cacheList[type]) {      
			this.cacheList[type] = [];    
		}    
		// 往对应的消息类型插入回调函数    
		this.cacheList[type].push(callback);  
		},  
		// 取消订阅  
		unsubscribe: function (type,callback){    
			if (!type) {// 直接调用,没有任何参数      
				this.cacheList = {};    
			} else if (type&&!callback) {// 删除某个消息类型的所有回调函数      
				delete this.cacheList[type];    
			} else {// 删除某个消息类型中的某个回调函数      
				this.cacheList[type] = this.cacheList[type].filter(function (cb){        
					return cb !== callback;      
				})    
			}  
		}
	}
	var obs = new Observe();

案例:网站登录

假如一个商城网站项目有header头部、nav导航、消息列表、购物车、地址管理等模块,这些模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的登录信息。如果它们和用户信息模块产生了强耦合,比如下面这样的形式:


$.ajax({    
	type: 'post',    
	url: 'https://www.baidu.com/',    
	data: 'user=xiaocuo&pass=123456',    
	dataType: 'json',    
	success: function (data) {        
		header.setAvatar(data.avatar); // 设置头部头像        
		nav.setAvatar(data.avatar); // 设置导航头像        
		address.refresh(); // 刷新收货地址列表        
		message.refresh(); // 刷新消息列表        
		cart.refresh(); // 刷新购物车列表        
		abc.refresh(data); // 刷新某某列表
        // ......    
      }
   })

我们必须知道header模块设置头像的方法叫setAvatar,刷新购物车列表的方法叫refresh,各种新增模块的方法…等等,我们会越来越疲于应付这些突如其来的业务要求!

用发布-订阅模式重构之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件


var loginEvent = { //登录成功的消息事件    
	clientList: {}, //缓存列表,存放订阅者的回调函数    
	addlisten: function (key,fn) { //添加订阅者        
		if (!this.clientList[key]) { //未订阅过此类消息,创建一个缓存列表            
			this.clientList[key] = [];        
		}        
		this.clientList[key].push(fn); //订阅的消息添加进消息缓存列表    
	},    
	trigger: function (key,msg) { //发布消息方法        
		var fnArr = this.clientList[key]; //取出该消息对应的回调函数集合       
		if (!fnArr || fnArr.length == 0) {            
			return false; // 如果未订阅该消息,则返回        
		}        
		for (var i = 0; i < fnArr.length; i++) {            
			fnArr[i](msg); //执行所有回调函数        
		}    
	}
} 
$.ajax({    
	type: 'post',    
	url: 'https://www.baidu.com/',    
	data: 'user=xiaocuo&pass=123456',    
	dataType: 'json',    
	success: function (data) {        
		loginEvent.trigger('loginSucc', data); // 发布登录成功消息    
	}
})
各个业务模块自己监听登录成功的消息:
var header = (function () { // 头部模块    
	loginEvent.addlisten('loginSucc', function (data) { //订阅登录成功的消息       
		 header.setAvatar(data.avatar);    
		});    
		return {        
			setAvatar: function (data) {            
				console.log('设置头部模块头像');        
			}    
		}
	})();
	var nav = (function () { // 导航模块    
		loginEvent.addlisten('loginSucc', function (data) { //订阅登录成功的消息        
			nav.setAvatar(data.avatar);    
		});    
		return {        
			setAvatar: function (data) {            
				console.log('设置导航模块头像');        
			}    
		}
	})();
	var address = (function () { // 地址模块    
		loginEvent.addlisten('loginSucc', function (obj) { //订阅登录成功的消息        
			address.refresh(obj);    
		});    
		return {        
			refresh: function (data) {            
				console.log('刷新收货地址列表');        
			}    
		}
	})();
	// ......

组合模式

  • 组合模式,就是把几个构造函数的启动方式组合再一起

  • 然后用一个 ”遥控器“ 进行统一调用

    class GetHome {
    
        init () {
            console.log('到家了')
        }
    }
    
    class OpenComputer {
    
        init () {
            console.log('打开电脑')
        }
    }
    
    class PlayGame {
    
        init () {
            console.log('玩游戏')
        }
    }
    • 上面几个构造函数的创造的实例化对象的 启动方式 都一致
    • 那么我们就可以把这几个函数以组合模式的情况书写
    • 然后统一启动
  • 准备一个 组合模式 的构造函数

    class Compose {
        constructor () {
            this.compose = []
        }
        	
        // 添加任务的方法
        add (task) {
            this.compose.push(task)
        }
        
        // 一个执行任务的方法
        execute () {
            this.compose.forEach(item => {
                item.init()
            })
        }
    }
  • 我们就用我们的组合模式构造函数来吧前面的几个功能组合起来

    const c = new Compose()
    // 把所有要完成的任务都放在队列里面
    c.add(new GetHome())
    c.add(new OpenComputer)
    c.add(new PlayGame)
    
    // 直接器动任务队列
    c.execute()
    // 就会按照顺序执行三个对象中的 init 函数

观察者模式

  • 观察者模式,通常也被叫做 发布-订阅模式 或者 消息模式
  • 英文名称叫做 Observer
  • 官方解释: 当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题
  • 听起来很迷糊,但是其实没有很难

一个例子

  • 当你想去书店买书,但是恰巧今天你要买的书没有了
  • 我们又不能总在书店等着,就把我们的手机留给店员
  • 当你需要的书到了的时候,他会打电话通知你,你去买了就好了
  • 你买到数了以后,就告诉他,我买到了,那么以后再来了书就不会通知你了

addEventListener

  • 上面的例子可能还不是很明确

  • 但是 addEventListener 是一个我们都用过的东西

  • 这个东西其实就是一个标准的 观察者模式

    btn.addEventListener('click', function () {
        console.log('btn 被点击了')
    })
    • 上面这个就是有一个 无形的观察者 再观察着 btn 的一举一动
    • 当这个 btn 被点击的时候,就会执行 对应的函数
    • 我们也可以多绑定几个函数
  • 说白了: 观察者模式就是我们自己实现一个 addEventListener 的功能

    • 只不过 addEventListaner 只有固定的一些事件,而且只能给 dom 元素绑定
    • 而我们自己写的可以随便绑定一个事件名称,自己选择触发时机而已

书写代码

  • 首先我们分析功能

    • 我们要有一个观察者(这里抽象为一个对象 {}

    • 需要有一个属性,存放消息的盒子(把你绑定的所有事件放在里面)

    • 需要一个 on 方法,用于添加事件

    • 需要一个 emit 方法,用于发布事件(触发)

    • 需要一个 off 方法,把已经添加的方法取消

      const observer = {
          message: {},
          on: function () {},
          emit: function () {},
          off: function () {}
      }
    • 我们把它写成一个构造函数的形式

      class Observer {
          constructor () {
              this.message = {}
          }
          
          on () {}
          
          emit () {}
          
          off () {}
      }
    • 现在,一个观察者的雏形就出来了

    • 接下来完善方法就可以了

ON
  • 先来写 ON 方法

  • 添加一个事件

  • 我们的 on 方法需要接受 两个参数

    • 事件类型
    • 事件处理函数
    class Observer {
        constructor () {
            this.message = {}
        }
        
        on (type, fn) {
            // 判断消息盒子里面有没有设置事件类型
            if (!this.message[type]) {
                // 证明消息盒子里面没有这个事件类型
                // 那么我们直接添加进去
                // 并且让他的值是一个数组,再数组里面放上事件处理函数
                this.message[type] = [fn]
            } else {
                // 证明消息盒子里面有这个事件类型
                // 那么我们直接向数组里面追加事件处理函数就行了
                this.message[type].push(fn)
            }
        }
        
        emit () {}
        
        off () {}
    }
EMIT
  • 接下来就是发布事件

  • 也就是让我们已经订阅好的事件执行一下

  • 同样需要接受两个参数

    • 要触发的事件类型
    • 给事件处理函数传递的参数
    class Observer {
        constructor () {
            this.message = {}
        }
        
        on (type, fn) {
            // 判断消息盒子里面有没有设置事件类型
            if (!this.message[type]) {
                // 证明消息盒子里面没有这个事件类型
                // 那么我们直接添加进去
                // 并且让他的值是一个数组,再数组里面放上事件处理函数
                this.message[type] = [fn]
            } else {
                // 证明消息盒子里面有这个事件类型
                // 那么我们直接向数组里面追加事件处理函数就行了
                this.message[type].push(fn)
            }
        }
        
        emit (type, ...arg) {
            // 判断你之前有没有订阅过这个事件
            if (!this.message[type]) return
    
            // 如果有,那么我们就处理一下参数
            const event = {
                type: type,
                arg: arg || {}
            }
    
            // 循环执行为当前事件类型订阅的所有事件处理函数
            this.message[type].forEach(item => {
                item.call(this, event)
            })
        }
        
        off () {}
    }
OFF
  • 最后就是移除事件

  • 就是把已经订阅的事件处理函数移除掉

  • 同样需要接受两个参数

    • 要移除的事件类型
    • 要移除的事件处理函数
    class Observer {
        constructor () {
            this.message = {}
        }
        
        on (type, fn) {
            // 判断消息盒子里面有没有设置事件类型
            if (!this.message[type]) {
                // 证明消息盒子里面没有这个事件类型
                // 那么我们直接添加进去
                // 并且让他的值是一个数组,再数组里面放上事件处理函数
                this.message[type] = [fn]
            } else {
                // 证明消息盒子里面有这个事件类型
                // 那么我们直接向数组里面追加事件处理函数就行了
                this.message[type].push(fn)
            }
        }
        
        emit (type, ...arg) {
            // 判断你之前有没有订阅过这个事件
            if (!this.message[type]) return
    
            // 如果有,那么我们就处理一下参数
            const event = {
                type: type,
                arg: arg || {}
            }
    
            // 循环执行为当前事件类型订阅的所有事件处理函数
            this.message[type].forEach(item => {
                item.call(this, event)
            })
        }
        
        off (type, fn) {
            // 判断你之前有没有订阅过这个事件
            if (!this.message[type]) return
    
            // 如果有我们再进行移除
            for (let i = 0; i < this.message[type].length; i++) {
                const item =  this.message[type][i]
                if (item === fn) {
                    this.message[type].splice(i, 1)
                    i--
                }
            }
        }
    }
  • 以上就是最基本的 观察者模式

  • 接下来我们就使用一下试试看

使用一下
const o = new Observer()

// 准备两个事件处理函数
function a(e) {
    console.log('hello')
}

function b(e) {
    console.log('world')
}

// 订阅事件
o.on('abc', a)
o.on('abc', b)

// 发布事件(触发)
o.emit('abc', '100', '200', '300') // 两个函数都回执行

// 移除事件
o.off('abc', 'b')

// 再次发布事件(触发)
o.emit('abc', '100', '200', '300') // 只执行一个 a 函数了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值