javascript设计模式

内容来自《javascript设计模式与开发实践》

一、单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
比如当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

实现:用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

const SingleTon = function(name) {
  this.name = name
}

SingleTon.prototype.getName = function() {
  console.log(this.name)
}

const instance = null
SingleTon.getInstance = function(name) {
  if(!this.instance) {
    this.instance = new SingleTon(name)
  }
  return this.instance
}

const a = SingleTon.getInstance('a')
const b = SingleTon.getInstance('b')
console.log(a===b) // true
console.log(a) // SingTon { name: 'a' }
console.log(b) // SingTon { name: 'a' }

通用惰性单例

const getSingle = function(fn) {
  const result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}
const bindEvent = getSingle(function() {
  document.getElementById('div1').addEventListener = function() {
    alert('click')
  }
  return true
})

const render = function() {
  bindEvent()
}

二、策略模式

定义:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对 Context 发起请求的时候,Context 总是把请求委托给这些策略对象中间的某一个进行计算。

使用策略模式计算奖金

// JavaScript 语言中,函数也是对象。所以将策略类定义成函数
const strategies = {
  'S': function(salary) {
    return salary * 4
  },
  'A': function(salary) {
    return salary * 3
  },
  'B': function(salary) {
    return salary *2
  }
}
// 用 calculateBonus 函数充当Context来接受用户的请求
const caculateBonus = function(level, salary) {
  return strategies[level](salary)
}

const a = caculateBonus('S', 10000)
console.log(a) // 4000

三、代理模式

定义:代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
虚拟代理实现图片预加载

 const myImage = (function() {
      const image = document.createElement('img')
      document.body.appendChild(image)
      return {
        setSrc: function(src) {
          image.src = src
        }
      }
    })()
    const proxyImage = (function() {
      const image = new Image()
      image.onload = function() {
        myImage.setSrc(this.src)
      }
      return {
        setSrc: function(src) {
          myImage.setSrc('loadingsrc')
          image.src = src
        }
      }
    })()
    proxyImage.setSrc('src')

意义:单一职责原则:一个类,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。 职责被定义为“引起变化的原因”。
且在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放-封闭原则。
代理和本体接口的一致性
用户可以放心的请求代理,他只关心能否得到想要的结果。
在任何使用本体的地方都可以替换成代理。
缓存代理
缓存代理可以为一些花销较大的运算结果提供暂时储存,下次运算时,如果传递进来的参数与之前一致,则可以直接返回之前的运算结果。
缓存计算乘积

const add = function() {
  let a = 0
  for(let i=0; i<arguments.length; i++) {
    a+=arguments[i]
  }
  return a
}

const mult = function() {
  let a = 1
  for(let i=0; i<arguments.length; i++) {
    a*=arguments[i]
  }
  return a
}

const createProxyFactory = function(fn) {
  const cache = {}
  return function() {
   const args = Array.prototype.join.call(arguments, '') // 使用call借用array的方法
   if(cache[args]) {
     return cache[args]
   }
   return cache[args] = fn.apply(this, arguments)
  }
}

const afunc = createProxyFactory(add)
const bfunc = createProxyFactory(mult)
console.log(afunc(1,2,3,4))
console.log(bfunc(1,2,3,4))

四、迭代器模式

定义:
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

五、发布-订阅模式

定义:发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知
全局的发布-订阅对象

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id='count'>click me</button>
  <div id='show'></div>
</body>
<script>
    // 发布-订阅的全局Event对象
    const Event = (function() {
     let clientList = {}, // 缓存列表,存放订阅者的回调函数
     trigger, listen, remove
     listen = function(key, fn) {
       if(!clientList[key]) {
         clientList[key] = [] // 如果没有订阅过此类消息,给该类消息创建一个缓存列表
       }
       clientList[key].push(fn) // 订阅的消息加入消息缓存列表
     }

     trigger = function() {
       const key = Array.prototype.shift.call(arguments) //取出消息类型
       const fns = clientList[key] // 取出该类型对应的订阅者回调函数
       if(!fns || !fns.length) { // 如果没有该订阅消息则返回
         return false
       }
       for(let i = 0; i<fns.length; i++) {
         const fn = fns[i]
         fn.apply(this, arguments)
       }
     }

     remove = function(key, fn) {
       const fns = clientList[key]
       if(!fns) { //如果key对应的消息没有被订阅,则直接返回
         return false
       }
       if(!fn) { // 如果没有指定具体函数,则取消key对应消息的所有订阅
         fns && (fns.length = 0)
       } else {
         for(let i=fns.length; i>0; i--) { // 反向遍历订阅列表
           const _fn=fns[l]
           if(fn === _fn) {
             fns.splice(1,1) // 删除订阅者的回调函数
           }
         }
       }
     }

     return {
       listen,
       trigger,
       remove
     }
   })()


   const a = (function(){
     let count = 0
     const button = document.getElementById('count')
     button.onclick = function() {
       Event.trigger('add', count++)
     }
   })()
   const b = (function(){
     const div = document.getElementById('show')
     Event.listen('add', function(count){
       div.innerHTML = count
     })
   })()
 </script>
</html>

六、命令模式

定义:
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。使用命令模式可以使得请求发送者和请求接收者能够消除彼此之间的耦合关系

例子:菜单程序

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id='button1'>refresh</button>
  <button id='button2'>add</button>
  <button id='button3'>delete</button>
</body>
  <script>
    // 用闭包实现命令模式
    const btn1 = document.getElementById('button1')
    const btn2 = document.getElementById('button2')
    const btn3 = document.getElementById('button3')
    // setCommand 负责往按钮上安装命令
    const setCommand = function(button, command) {
      button.onclick = () => {
        command.execute()
      }
    }
    // 按钮需要执行的行为
    const MenuBar = {
      refresh: () => {
        alert('refresh')
      }
    }
    const SubMenu = {
      add: () => {
        alert('add')
      },
      del: () => {
        alert('delete')
      }
    }
    // 将行为封装到命令类
    const RefreshMenuBarCommand = function(receiver) {
      return {
        execute: function() {
          receiver.refresh() // 接收者被封闭在闭包产生的环境中
        }
      }
    }
    const AddSubMenuCommand = function(receiver) {
      return {
        execute: function() {
          receiver.add()
        }
      }
    }
    const DelSubMenuCommand = function(receiver) {
      return {
        execute: function() {
          receiver.del()
        }
      }
    }
    // 将命令接受者传入到command对象中,并将command对象安装到button上面
    const refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
    const addSubMenuCommand = AddSubMenuCommand(SubMenu)
    const delSubMenuCommand = DelSubMenuCommand(SubMenu)
    setCommand(btn1, refreshMenuBarCommand)
    setCommand(btn2, addSubMenuCommand)
    setCommand(btn3, delSubMenuCommand)
  </script>
</html>

例子:重做

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id="replay">播放录像</button>
</body>
<script>
  const Ryu = { 
    attack: function(){
      console.log('攻击')
    },
    defense: function(){
      console.log('防御');
    },
    jump: function(){
      console.log('跳跃');
    },
    crouch: function(){
      console.log( '蹲下' );
    }
  }

  const makeCommand = function(receiver, state) { //创建命令
    return function() {
      receiver[state] && receiver[state]()
    }
  }

  const commands = {
    "119": "jump", // W
    "115": "crouch", // S
    "97": "defense", // A
    "100": "attack" // D
  }

  const commandStack = [] //保存命令的栈
  document.onkeypress = function(e) {
    const code = e.keyCode
    const command = makeCommand(Ryu, commands[code])
    if (command) {
      command() // 执行命令
      commandStack.push(command) // 保存命令
    }
  }

  document.getElementById('replay').onclick = function() {
    let command
    while(command = commandStack.shift()) {
      command()
    }
  }
</script>
</html>

七、组合模式

定义:
组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。
在这里插入图片描述
例子:扫描文件夹

// folder
const Folder = function(name) {
  this.name=name
  this.files=[]
}

Folder.prototype.add = function(file) {
  this.files.push(file)
}

Folder.prototype.scan = function() {
  console.log('start to scan folder', this.name)
  for(let i=0; i<this.files.length; i++) {
    const file = this.files[i]
    file.scan()
  }
}

//file
const File = function(name) {
  this.name = name
}

File.prototype.add = function() {
  throw new Error('cannot add for file')
}

File.prototype.scan = function() {
  console.log('start to scan file', this.name)
}

var folder = new Folder('学习资料')
var folder1 = new Folder('JavaScript')
var folder2 = new Folder ('jQuery')
var file1 = new File('JavaScript 设计模式与开发实践')
var file2 = new File('精通 jQuery')
var file3 = new File('重构与模式')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
// 在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。
folder.scan() // 从树顶端开始扫描

运行结果:
在这里插入图片描述
注意事项:
1.组合模式不是父子关系
组合模式是一种 HAS-A(聚合)的关系,而不是 IS-A。组合对象包含一组叶对象,但 Leaf
并不是 Composite 的子类。组合对象把请求委托给它所包含的所有叶对象,它们能够合作的关键
是拥有相同的接口。
2.对叶对象操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组
叶对象的操作必须具有一致性。
3.双向映射关系
对象之间的关系并不是严格意义上的层次结构时不适合使用组合模式
4.用职责链模式提高组合模式性能
借助职责链模式可以避免遍历整棵树,提高性能

七、享元模式

定义:
享元模式要求将对象的属性划分为内部状态与外部状态(属性)。享元模式的目标是尽量减少共享对象的数量。
内外部状态的划分:

  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

八、职责链模式

定义:
使多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止.
在这里插入图片描述
实现:
定义一个构造函数 Chain,在 new Chain 的时候传递的参数即为需要被包装的函数,同时它还拥有一个实例属性 this.successor,表示在链中的下一个节点。
Chain.prototype.setNextSuccessor 指定在链中的下一个节点
Chain.prototype.passRequest 传递请求给某个节点

var order500 = function( orderType, pay, stock ){ // ordertype: 订单类型, pay: 是否支付, stock:库存
  if ( orderType === 1 && pay === true ){ 
  console.log( '500 元定金预购,得到 100 优惠券' ); 
  }else{ 
  return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  } 
 }; 
 var order200 = function( orderType, pay, stock ){ 
  if ( orderType === 2 && pay === true ){ 
  console.log( '200 元定金预购,得到 50 优惠券' ); 
  }else{ 
  return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  } 
 }; 
 var orderNormal = function( orderType, pay, stock ){ 
  if ( stock > 0 ){ 
  console.log( '普通购买,无优惠券' ); 
  }else{ 
  console.log( '手机库存不足' ); 
  } 
 };

const Chain = function(fn) {
  this.fn = fn
  this.successor = null //在链中的下一个节点
}

Chain.prototype.setNextSuccessor = function(successor) { //指定在链中的下一个节点
  return this.successor = successor
}

Chain.prototype.passRequest = function() { // 传递请求给某个节点
  const ret = this.fn.apply(this, arguments)
  if(ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
  return ret
}

// 将订单函数包装成职责链的节点
var chainOrder500 = new Chain( order500 )
var chainOrder200 = new Chain( order200 )
var chainOrderNormal = new Chain( orderNormal )

// 设置节点在职责链的顺序
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

order500( 1 , true, 500); // 输出:500 元定金预购, 得到 100 优惠券
order500( 1, false, 500 ); // 输出:普通购买, 无优惠券
order500( 2, true, 500 ); // 输出:200 元定金预购, 得到 500 优惠券
order500( 3, false, 500 ); // 输出:普通购买, 无优惠券
order500( 3, false, 0 ); // 输出:手机库存不足

用 AOP 实现职责链:

var order500 = function( orderType, pay, stock ){ // ordertype: 订单类型, pay: 是否支付, stock:库存
  if ( orderType === 1 && pay === true ){ 
  console.log( '500 元定金预购,得到 100 优惠券' ); 
  }else{ 
  return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  } 
 }; 
 var order200 = function( orderType, pay, stock ){ 
  if ( orderType === 2 && pay === true ){ 
  console.log( '200 元定金预购,得到 50 优惠券' ); 
  }else{ 
  return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  } 
 }; 
 var orderNormal = function( orderType, pay, stock ){ 
  if ( stock > 0 ){ 
  console.log( '普通购买,无优惠券' ); 
  }else{ 
  console.log( '手机库存不足' ); 
  } 
 };

 Function.prototype.after = function( fn ){ 
  var self = this; 
  return function(){ 
  var ret = self.apply( this, arguments ); 
  if ( ret === 'nextSuccessor' ){ 
  return fn.apply( this, arguments ); 
  } 
  return ret; 
  } 
 };

var order = order500.after( order200 ).after( orderNormal ); 
order( 1, true, 500 ); // 输出:500 元定金预购,得到 100 优惠券
order( 2, true, 500 ); // 输出:200 元定金预购,得到 50 优惠券
order( 1, false, 500 ); // 输出:普通购买,无优惠券

九、中介模式

定义:
增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。
在这里插入图片描述

十、装饰器模式

定义:
装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式

模拟传统面向对象语言的装饰者模式:

// 原始的飞机类
const Plane = function() {}
Plane.prototype.fire = function() {
  console.log('发送普通子弹')
}
// 增加两个装饰类,分别是导弹和原子弹
const MissileDecorator = function(plane) {
  this.plane = plane
}
MissileDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('发射导弹')
}

const AtomDecorator = function(plane) {
  this.plane = plane
}
AtomDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('发射原子弹')
}

let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire()
// // 分别输出: 发射普通子弹、发射导弹、发射原子弹

导弹类和原子弹类的构造函数都接受参数 plane 对象,并且保存好这个参数,在它们的 fire方法中,除了执行自身的操作之外,还调用 plane 对象的 fire 方法。
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象.

改写对象或者对象的某个方法,并不使用“类”来实现装饰者模式:

var plane = { 
 fire: function(){ 
 console.log( '发射普通子弹' ); 
 } 
} 
var missileDecorator = function(){ 
 console.log( '发射导弹' ); 
} 
var atomDecorator = function(){ 
 console.log( '发射原子弹' ); 
} 
var fire1 = plane.fire; 
plane.fire = function(){ 
 fire1(); 
 missileDecorator(); 
} 
var fire2 = plane.fire; 
plane.fire = function(){ 
 fire2(); 
 atomDecorator(); 
} 
plane.fire(); 
// 分别输出: 发射普通子弹、发射导弹、发射原子弹

十一、状态模式

定义:
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。第一句话的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。比如电灯在 off 和 on 这两种不同的状态下,我们点击同一个按钮,得到的行为反馈是截然不同的。第二句话是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。

十二、适配器模式

定义:
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
例子:展示地图
假设googleMap展示地图用show方法,baiduMap用display方法

var googleMap = { 
 show: function(){ 
 console.log( '开始渲染谷歌地图' ); 
 } 
 }; 
var baiduMap = { 
 display: function(){ 
 console.log( '开始渲染百度地图' ); 
 } 
 }; 
var baiduMapAdapter = { 
 show: function(){ 
 return baiduMap.display();
 } 
 }; 
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图

包装模式:
适配器模式、装饰者模式、代理模式和外观模式都属于“包装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。

  • 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。
  • 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
  • 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。

十三、外观模式

定义:
外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过请求外观接口来达到访问子系统的目的。
在这里插入图片描述
外观模式的关键是定义一个高层接口去封装一组“子系统”。

var A = function(){ 
 a1(); 
 a2(); 
} 
var B = function(){ 
 b1(); 
 b2(); 
} 
var facade = function(){ 
 A(); 
 B(); 
} 
facade();

外观模式的作用

  • 为一组子系统提供一个简单便利的访问入口。
  • 隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值