JavaScript设计模式
本书同理摘抄自 曾探 著的《JavaScript设计模式与开发实践》。里面列举了专门针对JavaScript的16个设计模式,但是我没记住几个,基本就是看完了马上就忘了,所以为了方便自己回顾记忆就记录一下。但是因为自己再懒得看一遍,所以就基本上看每章的小结。本文所举得🌰都是JavaScript版本的例子,因为传统的设计模式以Java为例,但是java是传统的面向对象的语言,类是非常重要的东西,但是JavaScript类并不是非常的重要,对象和函数用的更频繁一点,所以JavaScript实现的也会不一样一点。
设计模式的一个准则就是开放——封闭原则,就是把“不变的事物”和“可能改变的事物”分离开来。
注意:这本书写的也算比较早了,是15年出版的,所以ES6语法基本上没有出现,有些写法还是旧的传统写法,但不妨碍学习设计模式,毕竟这玩意儿不怎么随着语言升级。
1、单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
比如线程池,全局缓存,浏览器的window对象,登录浮窗。
关键就是有一个变量来标志是否创建过对象,下面是一个比较好的获取单例对象的方法,用上了闭包和高阶函数。
var getSingle = function (fn) {
var result;
return function() {
return result || (result = fn.apply(this,arguments));
}
}
2、策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
一个策略模式的程序至少由两部分组成,第一个部分是一组策略组,策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类Context,Context接受客户的请求, 随后把请求委托个某一个策略类。
利用JavaScript,可以更简单和直接写出策略模式
下面举了一个奖金的计算。比如第一部分的策略组,直接使用对象
var strategies = {
"S" : function(salay) {
return salary * 4;
}
"A" : function(salay) {
return salary * 3;
}
"B" : function(salay) {
return salary * 2;
}
}
第二部分Context可以使用一个函数来表示
var calculateBonus = function (level,salary){
return strategies[level](salary);
}
//输出
console.log( calculateBonus( 'S', 20000 ) )
书本还举例了一个更加复杂的表单验证的策略模式,这里就不放了。
策略模式已经融入JavaScript了,比如上述的第一个部分strategies,直接去掉strategies,也算是一个策略模式
var S = function(salay) {
return salary * 4;
}
……
var calculateBonus = function (func,salary){
return func(salary);
}
//输出
console.log( calculateBonus( S, 20000 ) )
这里的strategy就是值为函数的变量,这在JavaScript比较隐形,利用函数对象的多态性,可以实现更简单。
3、代理模式
代理模式是为了一个对象提供一个代用品或占位符,以便控制对它的访问。
保护代理:代理B可以帮助A过滤掉一些请求
虚拟代理:代理B会在A需要的时候再去创建
JavaScript不容易实现保护代理,而虚拟代理是最常用的一种代理模式
比如图片加载代理,这里分成两块,虽然可以融合到一段代码里面,但是分成两部分,是为了面向对象设计的原则——单一职责原则。比如代理只负责预加载图片,预加载的操作完成之后,再把请求重新换给本体。proxy只做一件事情,就是预加载,将多个职责解耦,有利于维护,也符合开放——封闭原则。
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src){
imgNode.src = src;
}
}
})
var proxyImage = (function(){
var img = new Image;
img.onload = function(){ //监听网络图片,加载完成就替换myImage的src
myImage.setSrc(this.src);
}
return {
setSrc: function(src){
myImage.setSrc('file:///C:/Users/loading.gif') //设置占位的图片来提示用户图片正在加载,这会立即生效
img.src =src;
}
}
})();
proxyImage.setSrc('http://xxx/xxx.png');
还有一种缓存代理,比如这里有计算乘积的缓存代理,为一些开销大的运算结果提供暂时的存储
var mul = function(){
let a = 1;
for( let i=0;i =arguments.length;i<l;i++ ){
a = a * arguments [i];
}
return a;
}
/* 创建缓存代理的工厂,cache保存参数的字符串,如果有相同的那就直接返回 */
var createProxyFactory = function (fn) {
const cache = {};
return function(){
const args = Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args] = fn.apply(this,arguments);
}
}
}
const proxyMult = createProxyFactory(mult);
alert(proxyMult(1,2,3,4));
4、迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。(这块因为没有用到ES6的Symbol写迭代器,所以都是比较传统的写法,虽说如此,这种我才看得懂,Symbol.iterator我看不懂)
简单的迭代器,这种属于内部迭代器,each函数内部已经定义好了迭代规则,它接手整个迭代过程。
const each = function(arr,callback){
for(let i=0,l=arr.length;i<l;i++){
callback.call(ary[i],i,ary[i]);
}
}
each([1,2,3],function(i,n){
alert([i,n])
})
外部迭代器必须显示的请求迭代下一个元素,上述的内部迭代器写完了就不好比较两个数组是否相同
var Iterator = function(obj){
var current =0;
var next = function(){
current+=1;
}
var isDone = function(){
return current >= obj.length;
}
var getCurrItem = function(){
return obj[current];
}
return {
next,
isDone,
getCurrItem,
length:obj.length
}
}
var compare = function (iterator1,iterator2){
if(iterator1.length != iterator2.length){
alert('不相等')
}
while(!iterator1.isDone() !== iterator2.isDone()){
if(iterator.getCurrItem() !== iterator2.getCurrItem()){
throw new Error('不相同')
}
iterator1.next();
iterator2.next();
}
alert('相同')
}
但说实话,这个迭代器感觉性能应该非常的一般,感觉想要更厉害,还得是Symbol.iterator?
5、发布——订阅模式
又称为观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript中,一般用事件模型来代替传统的发布——订阅模式。
比如DOM事件就是一种,addEventListener
实现发布订阅模式三步走,这里以售楼处买房为例
- 首先要制定好谁当发布者(比如售楼处)
- 然后给发布者一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册)
- 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(挨个发短信)
这里给出发布订阅的通用实现
var event = {
clientList:{},
listen:function(key,fn){
if(!this.clentList[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);
}
}
}
再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布-订阅功能
var installEvent = function(obj){
for(let i in event){
obj[i] = event[i];
}
}
var salesOffices ={};
installEvent(salesOffices);
// 小明订阅消息
salesOffices.listen('88平',function(price){
console.log('价格',price);
})
// 售楼处打电话
salesOffices.trigger('88平',20000)
文章还给了一个复杂的先发布再订阅的例子,这里就不给了。
JavaScript实现发布订阅模式比较便利,因为可以传递函数,注册回调函数的形式来代替传统的发布订阅模式,会更加优雅。但订阅者本身要消耗一定的时间和内存,而且会弱化对象之间的联系,使用过度的话,导致程序会难以维护和理解。
6、命令模式
命令模式是最简单和优雅的模式之一,命令指的是一个执行某些特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接受这是谁,也不知道被请求的操作是什么。
JavaScript的命令模式,其实是回调函数的一个面向对象的替代品,在JavaScript语言中是一种隐形的模式。
这里面的🌰都比较碎,就不举例了,毕竟已经融入JavaScript了。
7、组合模式
组合模式是用小的对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。有种分治的感觉
最经典的例子就是扫描文件夹,文件夹和文件两个类型构建一个树类型的结构。
注意的点
- 组合模式不是父子关系,
- 对叶对象操作的一致性
- 双向映射关系(可以理解为一对一?)
- 用职责链模式提高组合模式性能
8、模板方法模式
模板方法模式是一种只需使用继承就可以实现的非常简单的模式,由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。
这部分感觉就是抽象类的概念了,只是JavaScript目前还没有抽象类的东西,TypeScript已经支持了,所以这块不讲了。
9、享元模式
享元模式是一种用于性能优化的模式,核心是运用共享技术来有效支持大量细粒度的对象。
享元模式要求将对象的属性划分成内部状态与外部状态(状态这里通常指属性),目标是尽量减少共享对象的数量。
- 内部状态存储于对象内部
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会改变
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
完成的对象是内部状态+外部状态共同组装成一个完整的对象,虽然花掉一定时间,但是却可以大大减少系统中的对象数量,因此享元模式是一种用时间换空间的优化模式
还有一种就是对象池,它和享元模式有一些相似之处,对象池就是把不用的对象存储起来,比如地图上的气泡,一开始的结果只有两个,后面的结果有6个,那么只需要再创建4个,之前的2个dom对象不需要重新创建了,只需要拿过来重新改一下innerHTML就行了。
10、职责链模式
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
比如公交车上的递卡帮忙刷一下就是一种职责链。
文章举的例子还是挺不错的,就是太长了,其中有一个通用的职责链节点,这里我改成es6语法
class Chain {
constructor(fn) {
this.fn = fn;
this.successor = null;
}
setNextSuccessor(successor) { // 设置下一条职责节点
this.successor = successor;
}
passRequest() {
let res = this.fn.apply(this, arguments); // 执行当前职责
if (res === 'nextSucessor') { // 如果不是当前职责,则传给下一个节点
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return res;
}
}
11、中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信。
文章给了两个例子,但都非常大,就不举例了。中介者模式的缺点就是中介者往往会变成一个巨大且难以维护的对象。而且要占去一部分内存
12、装饰者模式
装饰着模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
用AOP装饰函数,直接甩出两个方法。就是保存之前的状态,并把返回新经过装饰后的函数。
Function.prototype.before = function(beforefn){
let __self =this;
return function(){
beforefn.apply(this,arguments);
return __self.apply(this,arguments);
}
}
Function.prototype.after = function(afterfn){
let __self =this;
return function(){
let res = __self.apply(this,arguments);
afterfn.apply(this,arguments);
return res;
}
}
13、状态模式
状态模式的关键是区分事物内部的状态,事务内部状态的改变往往会带来事务的行为改变。
状态模式和策略模式很像,都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行,区别就是策略模式各个策略类质检室平等又平行的,它们之间没有任何联系,但状态模式之间对应的行为切换是已经被封装好的。
这里举的例子,用的委托技术来实现灯开关的状态机。
const FSM = {
off: {
buttonWasPressed() {
console.log('关灯');
this.buttonWasPressed.innerHTML = '下一次按我是开灯';
this.currState = FSM.on;
}
},
on: {
buttonWasPressed() {
console.log('开灯');
this.buttonWasPressed.innerHTML = '下一次按我是关灯';
this.currState = FSM.off;
}
}
};
class Light {
constructor() {
this.currState = FSM.off;
this.button = null;
}
init() {
const button = document.createElement('button'),
self = this;
button.innerHTML = '已关灯';
this.button = document.body.appendChild(button);
this.button.onclick = function () {
self.currState.buttonWasPressed.call(self);
};
}
}
14、适配器模式
作用是解决两个软件实体间的接口不兼容的问题。
比如下面这两种对象的转换
const guangdongCity=[
{
name:'shenzhen',
id:11,
},{
name:'guangzhou',
id:12
}
]
和
const guangdongCity=[
shenzhen:11,
guangzhou:12
]
就可以通过简单的转换
let address ={};
for(let i=0;i<oldAddress.length;i++){
address[oldAddress[i].name] = oldAddress[i].id;
}





