JavaScript是一种弱类型、动态的、基于原型的语言,由于语言的特殊性使得我们在书写JavaScript代码时候感觉到非常容易,甚至我们用很简单的方式都能实现一些设计模式,接下来我来简单的介绍几种常见的设计模式并利用JavaScript代码来实现它们
一、单体模式
单体模式:一个特定类的多个实例对象都指向同一个实例对象。
在JavaScript中没有类只有对象,当你创建一个对象时候并不会有其他对象与其类似,所以当我们用字面量创建一个对象时,其实就是一个JavaScript的单体例子。由于JavaScript具有new语法可使用构造函数来创建对象,当需要用new语法创建实例对象时,每个实例对象指向的实例对象并不会是同一个实例对象,为了让其获得指向完全相同对象的新指针,我们可以有一下方法实现它:
1、采用私用静态成员模式实现单体模式,其方法就是重写构造函数:
function Monomer(){
//缓存实例(私用静态成员变量)
var instance;
// 重写构造函数
Monomer = function Monomer(){
return instance
}
// 保留原型属性(在重写的构造函数中保存)
Monomer.prototype = this
// 实例
instance = new Monomer()
// 重置构造函数指针
instance.constructor = Monomer
//其他功能
instance.name = 'reai'
instance.age = 10
return instance;
}
//测试
var a = new Monomer()
var b = new Monomer()
a === b //true
//添加原型属性和方法
Monomer.prototype.learn = 'web'
Monnomer.prototype.getName = function(){ return this.name }
a.learn // 'web'
b.learn // 'web'
a.getName() // 'reai'
b.getName() // 'reai'
a.name = 'qq'
b.name // 'qq'
a.constructor === Monomer //true
2.采用立即执行函数包装实例和构造函数而实现
var Monomer
(function(){
var instance;
Monomer = function(){
if(instance) {
return instance
}
// 保留原型属性
instance = this
//其他功能
instance.name = 'reai'
instance.age = 10
}
}())
//测试 功能与上一种功能一样
var a = new Monomer()
var b = new Monomer()
a === b //true
a.constructor === Monomer //true
单体模式针对一个“类”仅创建一个对象,在javascript中所以的对象都是单体。在程序开发中的“单体”,指的是以模块模式创建的对象。
二、工厂模式
工厂模式:通过一个工厂函数来创建对象,用于处理创建相似对象时执行重复的操作,同时在编译时不知道具体类型的情况下,为工厂客户提供一种创建对象的接口。工厂模式创建对象有两种方式,其中之一就是自定义工厂函数,其二就是使用JavaScript内置Object()构造函数创建。
1、自定义工厂函数(以汽车制造商CarMaker为例)
// 构造函数
function CarMaker() { }
CarMaker.prototype.make = function () {
return '我将为汽车' + this.operation;
}
CarMaker.factory = function (type) {
var newCar;
// 判断CarMake中是否存在此方法
if (typeof CarMaker[type] !== 'function') {
throw {
name: 'Error',
message: type + "doesn't exist!"
}
}
// 判断当前方法中原型中是否有make方法,没有则继承父类,获取父类原型中make方法
if(typeof CarMaker[type].prototype.make !== 'function') {
CarMaker[type].prototype = new CarMaker()
}
// 返回当前方法的实例对象
return new CarMaker[type]()
}
CarMaker.frameWork = function () {
this.operation = '构架'
}
CarMaker.sprayLacquer = function () {
this.operation = '喷漆'
}
var worker1 = CarMaker.factory('frameWork')
var worker2 = CarMaker.factory('sprayLacquer')
worker1.make() //我将为汽车构架
worker2.make() //我将为汽车喷漆
2、Object()或new Object()创建对象
var a = new Object(1)
var b = new Object({})
var c = new Object('reai')
var d = new Object(true)
var e = Object([])
a.constructor === Number //true
b.constructor === Object //true
c.constructor === String //true
d.constructor === Boolean //true
e.constructor === Array //true
工厂模式主要是根据字符串的类型在运行工厂函数时创建对象的方法。
三、迭代器模式
迭代器模式:提供一个API来遍历或者操纵复杂自定义的数据结构。在迭代器中一般包含数据结构的集合,迭代器模式提供一种简单的方式能够访问复杂的数据结构中的每个节点元素。实现方法如下:
var iterator = (function(){
var index = 0,
data = [1,2,3,4,5,6,7,8,9] //数据结构
length = data.length
return {
// 获取下一个元素
next: function(){
var el;
if(!this.hasNext()) { return null; }
el = data[index];
index++;
return el
},
// 判断下一个元素是否存在
hasNext: function(){
return index < length
},
// 初始化数据
rewind: function(){
index = 0;
},
// 获取当前元素
current: function(){
return data[index]
}
}
}())
// 测试
while(iterator.hasNext()){ console.log(iterator.next()) } // 输出1 2 3 4 5 6 7 8 9
iterator.rewind() //初始化数据
iterator.current() //1
这里只是一个简单的例子,在处理不同数据获取不同数据时,可以自定义获取数据的规则,以及加入一些利于数据获取的其他方法。
四、装修者模式
装修者模式:代码运行时动态的添加附件功能到对象中,并且可定制和可配置预期行为。其实过程很简单,比如我是一个房屋装修者,房屋主人让我铺地板、粉刷墙、安装玻璃、摆放家具等全部房间装修,并且每天工作完成都会根据我做的事结算工资。假如今天我做了铺地板和安装玻璃,我将会把这两项加入今天结算的工资行列,最后进行一次汇总得到今天的工资,装修者模式就是为了干这个事。下面用代码来实现装修者模式:
1、通过继承来实现
function Salary(price){
this.price = price
this.daySalary = 0
}
Salary.prototype.getSalary= function(){
return this.daySalary
}
Salary.decorators = {}
Salary.prototype.decorate = function(decorators){
var F = function() {},
overrides = this.constructor.decorators[decorators],
i, newObj;
// newObj继承Salary
F.prototype = this
newObj = new F()
newObj.uber = F.prototype
// 复制decorators中的每一项
for(i in overrides) {
if(overrides.hasOwnProperty(i)){
newObj[i] = overrides[i]
}
}
return newObj
}
// 安地板
Salary.decorators.flooring = {
getSalary: function() {
var price = this.uber.price
var salary = this.uber.getSalary()
salary += price * 0.1 ;
return salary
}
}
// 装玻璃
Salary.decorators.plastic = {
getSalary: function() {
var price = this.uber.price
var salary = this.uber.getSalary()
salary += price * 0.2 ;
return salary
}
}
// 粉刷墙
Salary.decorators.paintWall = {
getSalary: function() {
var price = this.uber.price
var salary = this.uber.getSalary()
salary += price * 0.3 ;
return salary
}
}
var a =new Salary(20000)
console.log(a.decorate('flooring').decorate('plastic').getSalary()) //铺地板2000,安装玻璃4000-> 输出6000
分析以上代码:执行decorate都有会继承父类,所以plcastic继承flooring,flooring继承a,在执行getSalary()方法的时候先执行plastic的getSalary()改变daySalary为4000,随后执行flooring的getSalary()累加daySalary为4000+2000 = 6000,最后执行a中的getSalary()方法返回daySalary,所以结果为6000
2、使用列表实现
function Salary(price){
this.price = price
this.daySalary = 0
this.decorators_list = [] // 装修列表
}
Salary.prototype.getSalary= function(){
var salary = this.daySalary,
i,
max = this.decorators_list.length;
// 遍历执行所有装修列表的getSalary方法
for(i =0; i < max; i++) {
salary = Salary.decorators[this.decorators_list[i]].getSalary(salary,this.price)
}
return salary
}
Salary.decorators = {}
Salary.prototype.decorate = function(decorator){
this.decorators_list.push(decorator)
}
// 安地板
Salary.decorators.flooring = {
getSalary: function(salary,price) {
salary += price * 0.1 ;
return salary
}
}
// 装玻璃
Salary.decorators.plastic = {
getSalary: function(salary,price) {
salary += price * 0.2 ;
return salary
}
}
// 粉刷墙
Salary.decorators.paintWall = {
getSalary: function(salary,price) {
salary += price * 0.3 ;
return salary
}
}
var a=new Salary(20000)
a.decorate('plastic')
a.decorate('flooring')
a.getSalary() // 分别执行plastic、flooring中的getSalary()方法并返回salary4000+2000 -> 6000
代码分析:Salary.decorators对象中写入各个方法,decorators_list数组保存添加属性名称,通过遍历decorators_list执行Salary.decorators对象中对应属性的getSalary方法,并把每次计算的salary值带入每次getSalary方法中,实现累加计算,最后返回salary
五、策略模式
策略模式:在代码运行时选择最优的策略(算法)去处理任务,在策略模式中常常运用到的场景在数据的验证上,创建一个验证器对象,验证器选择最佳的策略来处理任务,并且将具体的数据验证为他给适当的算法。下面用策略模式来写一个数据验证案例:
var validator = {
// 所有可用的检查
types: {
// 验证为空
isNonEmpty: {
validate: function (val) {
return val || val !== '';
},
instructions: 'the value cannot be empty'
},
// 验证数字
isNumber: {
validate: function (val) {
return !isNaN(val)
},
instructions: 'the value can only be a valid number, e.g. 1, 3.14'
},
// 验证字母和数字
isAlphaNum: {
validate: function (val) {
return !/[^a-zA-Z0-9]/i.test(val)
},
instructions: 'the value can only contain characters and numbers, no special symbols'
}
//更多验证规则
//...
},
// 当前验证会话中的错误信息
messages: [],
// 验证配置:名称:验证类型
config: {},
// 接口方法
validate: function (data) {
var type, msg, checker, res_ok;
this.messages = [];
for (let i in data) {
if (data.hasOwnProperty(i)) {
type = this.config[i];
checker = this.types[type];
if (!type) {
continue;
}
if (!checker) {
throw {
name: 'validation error',
message: 'No handler to validate type' + type
}
}
// 调用自身验证器验证规则
res_ok = checker.validate(data[i]);
if (!res_ok) {
msg = `Invalid Value for * ${i} *, ${checker.instructions}`;
this.messages.push(msg)
}
}
}
return this.hasErrors()
},
// 错误检查
hasErrors: function () {
return this.messages.length !== 0
}
}
//测试
var data = {
name: 'reai',
age: '10',
birthday: ''
}
// 配置验证规则
validator.config = {
name: 'isNumber',
age: 'isAlphaNum',
birthday: 'isNonEmpty'
}
validator.validate(data)
if(validator.hasErrors()){
console.log(validator.messages.join("\n"))
}
// 输出
// Invalid Value for * name *, the value can only be a valid number, e.g. 1, 3.14
// Invalid Value for * birthday *, the value cannot be empty
六、外观模式
外观模式:一种很普通的模式,通常把常用方法包装到一个特定的新方法种,提供一个可供选择的接口。实现外观模式可以让我的代码变得更加简洁,增强代码可读性,对于我们重新设计与重构工作非常有意义,接下来实现一个简单的外观模式:
// 包装常用事件行为
var event = {
// 阻止事件默认和冒泡行为
stop: function(e){
if(typeof e.preventDefault === 'function') {
e.preventDefault()
}
if(typeof e.stopPropagation === 'function') {
e.stopPropagation()
}
// IE兼容处理
if(typeof e.returnValue === 'boolean') {
e.returnValue = false
}
if(typeof e.cancelBubble === 'boolean') {
e.cancelBubble = true
}
}
// 其他方法
// ...
}
七、代理模式
代理模式:通过包装一个对象以控制对它的访问,主要方法是将访问聚集为组或仅当正真要的时候才执行访问,从而避免了高昂的操作开销。这里介绍的代理模式并不是ES6中实现的proxy,主要是介绍模式的原理。代理模式理解很简单,就是我们有时候需要向服务器连续发生很多请求,这时候我们可以从中许多重复请求抽离成一个请求,然后在对服务器进行访问,服务器响应后通过回调proxy中的handler方法返回数据到请求端。
var http = {
makeRequest:function(ids,callback) {
// 封装jsonp
// ids为请求的id组,callback回调函数
// ...
}
}
var proxy = {
ids: [],
delay: 50,
timeout: null,
callback: null,
context: null,
makeRequest: function(id, callback, context) {
this.ids.push(id);
this.callback = callback;
this.context = context;
// 默认设置50ms去收集所有的请求id
if(!this.timeout) {
this.timeout = setTimeout(() => {
proxy.flush()
}, this.delay)
}
},
flush: function() {
// 发送http请求
http.makeRequest(this.ids, 'proxy.handler')
// 清除超时设置和队列
this.timeout = null;
this.ids = []
},
handler: function(data){
var i, max;
// 单个请求
if(parseInt(data.query.count, 10) === 1) {
proxy.callback.call(proxy.context, data.query.results)
}
// 多个请求
for(i = 0, max = data.query.results.length; i < max; i++){
proxy.callback.call(proxy.context, data.query.results[i])
}
}
}
var obj = {
getData: function(data){
// 回调获取的数据
},
http: function(id){
http.makeRequest(id, 'obj.getData')
}
}
上面代码是伪代码,是无法真实运行的,只是模拟了代理过程。分析代码如下:
如果我们每次请求都需要通过obj.http传入id,连续传入不同id分别执行,不使用代理模式下,我们会在getData回调下拿到数据,但是每次执行obj.http我们都需要一次服务器的请求;如果我们使用proxy.makeRequire请求多次,此时proxy对象内部会在一段时间内对我们请求的id进行一次汇总,然后在进行服务器请求,服务器响应数据后,并通过getData进行返回数据。这就是代理模式的过程,避免了多次相似请求服务器额外的开销,并且在代理模式下我们可以设置缓存代理,(实现方法也很简单,例子中没有体现)将每次请求的数据进行保存在proxy.cache中,当有相关的请求一些静态资源时候,我们可以从缓存中取出相应数据进行响应,避免了再次请求服务器带来的开销。
八、中介者模式
中介者模式:指对象相互并不会直接通话,而是通过一个中介者对象进行通信,从而促成对象间的松散耦合,这种模式提高了代码的可维护性。下面举一个例子实现中介者模式,实现两个对象之间的通信:
// 一个中介者对象
var mediator = {
objs: {},
events: {},
// 初始化参与对象,并且绑定发送和监听事件
init: function (member) {
for (let i in member) {
member[i].send = mediator.update
member[i].on = mediator.listen
mediator.objs[i] = member[i]
}
},
// 广播发送信息
update(msg, to) {
mediator.broadcast(this.name, msg, to)
},
// 广播函数 异步处理
broadcast(from, msg, to) {
// 模拟异步发送
setTimeout(() => {
console.log(from + '发送信息: ' + msg)
mediator.events[to] && mediator.events[to](to + ' 收到信息来自' + from + '信息: ' + msg)
}, 0)
},
// 添加监听事件
listen: function (fn) {
mediator.events[this.name] = fn
}
}
// 测试
// 人物1对象
var preson1 = {
name: 'reai'
}
// 人物2对象
var preson2 = {
name: 'zqy'
}
// 中介者初始化加入对象
mediator.init({ preson1: preson1, preson2: preson2 })
// 向用户zqy发送信息为123
preson1.send('123', 'zqy')
// 监听别人向reai发送的信息
preson1.on(res => {
console.log(res) // 输出456
})
// 监听别人zqy向发送的信息
preson2.on(res => {
console.log(res) //输出123
})
// 向用户reai发送信息为456
preson2.send('456', 'reai')
在中介者模式下,中介者对象就是其他对象的载体,让相互对象不能直接获取到相互的信息,只有经过中介者才能散发信息,彼此的信息在没有经过中介者都是独立的,以上通过中介者模式实现了两个对象的相互的通信,其实解决办法也算勉强,接下来我们学习观察者模式,更加利于我们解决这些问题。
九、观察者模式
观察者模式:通过创建观察对象,当发生一个感兴趣的事件时可以将该事件告诉给所有的观察者,从而形成松散耦合,观察者模式广泛应用与浏览器事件中(比如鼠标的悬停、按键等),观察者模式别名也叫做订阅/发布模式。接下来我们通过案例来实现观察者模式。
var publisher = {
//订阅者事件
subscribers: {
any: [] // 未设置事件类型时存储事件
},
// 写入订阅事件
subscribe: function (fn, type) {
type = type || 'any';
if (typeof this.subscribers[type] === "undefined") {
this.subscribers[type] = [];
}
this.subscribers[type].push(fn);
},
// 拜访订阅事件
visitSubscribers: function (action, arg, type) {
var pubtype = type || 'any', // 发布类型
subscribers = this.subscribers[pubtype], //订阅事件数组
i,
max = subscribers.length;
for (i = 0; i < max; i += 1) {
if (action === 'publish') {
subscribers[i](arg);
} else {
if (subscribers[i] === arg) {
subscribers.splice(i, 1);
}
}
}
},
// 移除订阅事件
unsubscribe: function (fn, type) {
this.visitSubscribers('unsubscribe', fn, type);
},
// 发布信息
publish: function (publication, type) {
this.visitSubscribers('publish', publication, type);
},
// 创建发布者
clone(obj) {
var i;
for (i in publisher) {
if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
obj[i] = publisher[i];
}
}
obj.subscribers = { any: [] };
}
}
var person1 = { }
publisher.clone(person1) // 创建发布者
var person2 = {
on:function(res) {
console.log(res) // hello reai
},
get:function(res) {
console.log(res) // get:hello reai
}
}
// 订阅者person2订阅的事件on为any
person1.subscribe(person2.on);
// 订阅者person2订阅的事件get为get
person1.subscribe(person2.get,'get');
// 发布者创建发布信息函数
person1.send = function(msg,type) {
this.publish(msg,type)
}
// 在any模式下发布信息
person1.send('hello reai')
// 在get模式发布信息
person1.send('get:hello reai','get')
//注销any模式下所有的person2.on事件
person1.unsubscribe(person2.on)
//注销get模式下所有的person2.on事件
person1.unsubscribe(person2.get,'get')
由观察者模式和中介者模式可以看出,观察者和中介者模式非常相似,因为他们都是促进对象间的通信,同时又能使对象间松散耦合,但是它们在设计模式上存在一定的差异。中介者模式强调的是参与者彼此之间的交流,并且中介者必须先前知道所有的参与者;而观察者模式强调观察者改变成发布目标后对观察者进行统一的通讯,先前不需要知道自己由多少观察者,只知道发布事件,而观察者必须要知道发布目标。
谢谢大家阅读,本问主要参考Javascript设计模式书中介绍的九种设计模式,如有错误和疑问的地方可以在评论区留言,喜欢的话点个赞加入收藏qq:1367478101✋✋✋