javaScript设计模式(详解原文版)

目录

前言

一、单例模式

1.基于类的概念的单例模式

2.惰性单例

3.单例模式总结

二、策略模式

1.传统面向对象的实现方式(多态性的体现)

2.JavaScript版本的策略模式

3.实际业务场景中运用(表单校验为例)

3.1通过策略模式改编后

3.2深入策略模式,添加多种校验。

三、代理模式

1.模拟送花的场景

2.利用虚拟代理实现图片预加载

3.利用虚拟代理合并HTTP请求(this)

4.利用缓存代理计算复杂运算

5.利用高阶函数动态创建代理 

四、迭代器模式

1.实现自己的迭代器

2.内部迭代器和外部迭代器

2.1内部迭代器

2.2外部迭代器

3.倒叙迭代器

4.中止迭代器

5.在项目中优化的应用

五、发布订阅模式

1. 模拟售楼部的发布订阅场景

2. 发布订阅的通用实现

3. 真实例子-网站登录

4. 全局的发布订阅模式与模块间的通信

六、命令模式

1. JavaScript中的命令模式

2. 撤销和重做

3. 命令队列

4. 宏命令(下一章节将为大家介绍更高级的宏命令,此处仅为引申帮大家了解命令队列的执行机制)

七、组合模式

1. 更强大的宏命令 

2. 模仿扫描文件的组合模式

八、模板方法模式

 1. 泡咖啡、泡茶的例子

2. 用模板方法构建(附带钩子函数)

3.高阶函数是更好的选择(比上面的代码快0.1ms)

九、享元模式

 1. 腾讯微云上传案例(未优化)

2. 通过享元模式优化(之前会创建多个对象 现在同时上传2000个也只创建两个对象)

3. 对象池

十、职责链模式

1. 模拟商品订单处理 

2. 灵活可拆分的职责链节点

3. 用AOP实现职责链(相当的优雅)

十一、中介者模式

1. 泡泡堂案例 --双人对战

2. 泡泡堂案例 --多人对战

3. 泡泡堂案例 --引入中介者模式实现更多功能

4.购买商品案例

 十二、装饰者模式

1. 利用AOP方式实现 

2. AOP的使用案例

3. AOP动态改变函数的参数

3. 插件式表单验证

十三、状态模式

1.电灯案例

2.文件上传案例

3.javascript版本的开关灯案例

4.闭包形式的开关灯

十四、适配器模式

1.渲染地图的例子

2,渲染广东地图的例子


前言

设计模式,上层应用体现不出太多,但其中的思想可以运用到方方面面。这里记录一下我自己的学习心得,分享出来。

此为系列文章,坚持每天更新

一、单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点

页面中的登录按钮无论点击多少次 都只会创建一次登录浮窗,适合使用单例模式

1.基于类的概念的单例模式

// 普通类
var CreateDiv = function (html) {
  this.html = html
  this.init()
}
CreateDiv.prototype.init = function () {
  var div = document.createElement("div")
  div.innerHTML = this.html
  document.body.appendChild(div)
}
// 引入代理类(管理单例的逻辑)
var ProxySingletonCreateDiv = (function () {
  var instance
  return function (html) {
    if (!instance) instance = new CreateDiv(html)
    return instance
  }
})()

var a = new ProxySingletonCreateDiv('tom')
var b = new ProxySingletonCreateDiv('jerry')
console.log(a === b) // true; 都是tom

2.惰性单例

传统的登录在页面初始化的时候创建登录浮窗节点...

var loginLayer = (function () {
  var div = document.createElement("div");
  div.innerHTML = "我是登录弹窗";
  div.style.display = 'none'
  document.body.appendChild(div);
  return div;
})()

document.getElementById("loginBtn").onclick = function () {
  loginLayer.style.display = 'block';
};

上述方式有一个问题,也许用户根本不需要登录,就可能白白浪费一些DOM节点

var createLoginLayer = function () {
  var div = document.createElement("div");
  div.innerHTML = "我是登录弹窗";
  div.style.display = 'none'
  document.body.appendChild(div);
  return div;
}
document.getElementById("loginBtn").onclick = function () {
  var loginLayer = createLoginLayer();
  loginLayer.style.display = 'block';
};

这样虽然达到了惰性的目的 但是失去了单例效果,虽然可以加一个关闭的指令 但是在页面中频繁的创建删除节点是不必要的,此时可以将createLoginLayer函数改成如下用变量控制

var createLoginLayer = (function () {
  var div
  return function () {
    if (!div) {
      div = document.createElement("div");
      div.innerHTML = "我是登录弹窗";
      div.style.display = 'none'
      document.body.appendChild(div);
    }
    return div
  }
})()

以上,仍然是违反单一原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内的,如果我们下次需要创建页面中唯一的iframe或者script,用来跨域请求数据,就必须再把createLoginLayer几乎照抄一遍。

// 换成iframe...
var createIframe = (function () {
  var iframe
  return function () {
    if (!iframe) {
      iframe = document.createElement("iframe");
      iframe.innerHTML = "我是登录弹窗";
      iframe.style.display = 'none'
      document.body.appendChild(iframe);
    }
    return iframe
  }
})()

现在就把管理单例的逻辑抽出来,创建对象的方法当成参数动态传入getSingle函数

// 管理单例模式函数
// result在一个闭包环境中永远不会被销毁
var getSingle = function (fn) {
  var result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}
// 逻辑处理函数
var createLoginLayer = function () {
  var div = document.createElement("div");
  div.innerHTML = "我是登录弹窗";
  div.style.display = 'none'
  document.body.appendChild(div);
  return div;
}
var createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById("loginBtn").onclick = function () {
  var loginLayer = createSingleLoginLayer()
  loginLayer.style.display = 'block';
  // var loginLayer = createSingleIframe()
  // loginLayer.src = 'http://www.baidu.com/'
};
// 再试试iframe
// var createSingleIframe = getSingle(function () {
//   var iframe = document.createElement("iframe")
//   document.body.appendChild(iframe)
//   return iframe
// })

3.单例模式总结

在这个例子里我们把创建实例对象的职责和管理单例的职责放在两个方法里, 这两个方法可以独立变化互不影响,当他们连接在一起的时候 就完成了创建唯一实例对象的功能,才具有单例模式的威力。在合适的时候才创建对象,并且只创建唯一的一个。

二、策略模式

定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。说人话就是定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对context发起请求的时候,context吧请求委托给策略对象的某一个进行计算。

接下来看一个模拟计算奖金的场景

var calculateBouns = function (performanceLevel, salary) {
  if (performanceLevel === 'S') salary * 4
  if (performanceLevel === 'A') salary * 3
  if (performanceLevel === 'B') salary * 2
}
calculateBouns('B', 14000)

这是很常见的编码形式,可以看到缺乏弹性 复用性差 违反开放封闭原则

1.传统面向对象的实现方式(多态性的体现)

// 组合,委托,多态
// 绩效类
var PerformanceS = function () { }
performanceS.prototype.calculate = function (salary) {
  return salary * 4
}

var PerformanceA = function () { }
performanceA.prototype.calculate = function (salary) {
  return salary * 3
}


var PerformanceB = function () { }
performanceB.prototype.calculate = function (salary) {
  return salary * 2
}

// 奖金类
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.getBouns = function () {
  if (!this.strategy) throw new Error('未设置strategy属性')
  return this.strategy.calculate(this.salary)
}

// Bouns本身没有计算能力,而是委托给保存好的策略对象
var bouns = new Bonus
bouns.setSalary(14000)
bouns.setStrategy(new PerformanceB)
console.log(bouns.getBouns());

2.JavaScript版本的策略模式

var strategies = {
  "S": function (salary) {
    return salary * 4
  },
  "A": function (salary) {
    return salary * 3
  },
  "B": function (salary) {
    return salary * 2
  }
}
var calculateBouns = function (level, salary) {
  return strategies[level](salary)
}

3.实际业务场景中运用(表单校验为例)

<body>
  <form action="*" method="post" id="registerForm">
    用户名:<input type="text" name="uname">
    密码:<input type="text" name="pwd">
    手机号:<input type="text" name="phoneNum">
    <button>提交</button>
  </form>
</body>
<script>
  var registerForm = document.getElementById("registerForm")
  registerForm.onsubmit = function () {
    if (registerForm.uname === "") {
      alert("Please enter your name")
    }
    if (registerForm.pwd.value.length < 6) {
      alert("Do not 6")
    }
    // ...
  }
</script>

3.1通过策略模式改编后


  var strategies = {
  isNonEmpty: (val, errMsg) => { // 不为空
    if (val === '') {
      return errMsg
    }
  },
  minLength: (val, len, errMsg) => { // 最小长度
    if (val.length < len) {
      return errMsg
    }
  },
  isMobile: (val, errMsg) => { // 手机号格式
    if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) {
      return errMsg
    }
  }
}

// 实现Validator类,作为Context 接收用户请求并委托给strategies,先了解如何实现业务的过程
// 在编写Validator类的代码
var validataFunc = function () {
  var validator = new Validator()
  // 添加一些校验规则
  validator.add(registerForm.uname, 'isNonEmpty', '用户名不能为空')
  validator.add(registerForm.pwd, 'minLength:6', '密码不能少于6')
  validator.add(registerForm.phoneNum, 'isMobile', '手机号不符合')

  var errorMsg = validator.start() // 启动校验获得校验结果
  return errorMsg // 如确切反悔了errorMsg 说明没有通过校验
}

// 客户调用代码
var registerForm = document.getElementById('registerForm')
registerForm.onsubmit = () => {
  var errorMsg = validataFunc() // 如果有确切返回值说明未通过校验
  if (errorMsg) {
    alert(errorMsg)
    return false // 阻止表单提交
  }
}

// validator类的实现
var Validator = function () {
  this.cache = [] // 保存校验规则
}

Validator.prototype.add = function (dom, rule, errorMsg) {
  var ary = rule.split(':')
  this.cache.push(function () {
    var strategy = ary.shift() // 客户挑选的strategy
    ary.unshift(dom.value)
    ary.push(errorMsg)
    return strategies[strategy].apply(dom, ary)
  })
}

Validator.prototype.start = function () {
  for (var i = 0, validataFunc; validataFunc = this.cache[i++];) {
    var msg = validataFunc() // 开始校验 并取得返回信息
    if (msg) {
      return msg
    }
  }
}

3.2深入策略模式,添加多种校验。

validator.add(registerForm.uname, [
  {
    strategy: 'isNonEmpty', errMsg: '不能为空' 
  }, 
  { 
    strategy: 'minLength:6', errMsg: '不能少于6'
  }])

只需要改动一下add方法

Validator.prototype.add = function (dom, rules) {
  var _this = this
  for (var i = 0, rule; rule = rules[i++];) {
    (function (rule) {
      var strategyAry = rule.strategy.split(':')
      var errorMsg = rule.errMsg
      _this.cache.push(function () {
        var strategy = strategyAry.shift()
        strategyAry.unshift(dom.value)
        strategyAry.push(errorMsg)
        return strategies[strategy].apply(dom, strategyAry)
      })
    })(rule)
  }
}

三、代理模式

面向对象设计鼓励将行为分布到细粒度的对象之中,
如果一个对象承担的职责过多,等于把这些职责耦合到一起,这种耦合会导致脆弱和低内聚的设计,当变化发生时,设计可能会遭到意外的破坏

1.模拟送花的场景

简单理解一下场景:小明要给A送花 但是不好意思所以委托给了B

var Flower = function () { }

var xiaoming = {
  sendFlower: function (target) {
    var flower = new Flower()
    target.receiveFlower(flower)
  }
}

var A = {
  receiveFlower: function (flower) {
    console.log('收到花' + flower);
  }
}

// xiaoming.sendFlower(A)

// 引入代理
var B = {
  receiveFlower: function (flower) {
    A.receiveFlower(flower)
  }
}
xiaoming.sendFlower(B)

假设A心情好成功率60% 心情不好无限趋近与0,可以让B监听A的心情。可以看到A心情好的时候才触发创建花的类

    var Flower = function () { }

    var xiaoming = {
      sendFlower: function (target) {
        var flower = new Flower()
        target.receiveFlower(flower)
      }
    }

    var A = {
      receiveFlower: function (flower) {
        console.log('收到花' + flower);
      },
      listenGoodMood: function (fn) {
        setTimeout(function () { // 10秒会有好心情
          fn();
        }, 10000)
      }
    }

    // xiaoming.sendFlower(A)

    // 引入代理
    var B = {
      receiveFlower: function (flower) {
        A.listenGoodMood(function () { // 监听A的好心情
          var flower = new Flower() // 虚拟代理
          A.receiveFlower(flower)
        })
      }
    }
    xiaoming.sendFlower(B)

注意:代理可以做到的远不止这一点

2.利用虚拟代理实现图片预加载

var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);

  return {
    setSrc: function (src) {
      imgNode.src = src;
    }
  }
})()
// 可以看到网速慢的情况下有空白现象
myImage.setSrc('https://t7.baidu.com/it/u=963301259,1982396977&fm=193&f=GIF')


var proxyImage = (function () {
  var img = new Image
  img.onload = function () {
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function (src) {
      myImage.setSrc('./切图2/home1.png')
      img.src = src
    }
  }
})()
proxyImage.setSrc('https://t7.baidu.com/it/u=963301259,1982396977&fm=193&f=GIF')

3.利用虚拟代理合并HTTP请求(this)

<!--  没用代理的时候 对服务器开销是很大的 -->
<body>
  <input type="checkbox" id="1">1
  <input type="checkbox" id="2">2
  <input type="checkbox" id="3">3
  <input type="checkbox" id="4">4
  <script>
    var synchronousFile = function (id) {
      console.log('开始同步文件夹,id为' + id);
    }
    var checkbox = document.getElementsByTagName('input')
    for (var i = 0, c; c = checkbox[i++];) {
      c.onclick = function () {
        if (this.checked === true) {
          synchronousFile(this.id)
        }
      }
    }
    // 此时加入代理,将客户调用的synchronousFile改成该代理函数调用就行
    var proxySunchronousFile = (function () {
      var cache = [], timer
      return function (id) {
        cache.push(id)
        if (timer) return
        timer = setTimeout(function () {
          synchronousFile(cache.join(','))
          clearTimeout(timer)
          timer = null
          cache.length = 0
        }, 2000)
      }
    })()
  </script>
</body>

 注意:此处开始引入了缓存的概念

4.利用缓存代理计算复杂运算

// mult可以专注于自己的职责-计算 缓存由代理实现
var mult = function () {
  var a = 1
  for (var i = 0; i < arguments.length; i++) {
    a = a * arguments[i]
  }
  console.log('我要计算了');
  return a
}

// 现在加入缓存代理函数 
var proxyMult = (function () {
  var cache = {}
  return function () {
    var args = Array.prototype.join.call(arguments, ',')
    if (args in cache) { // 缓存
      return cache[args] // 返回值
    }
    return cache[args] = mult.apply(this, arguments)
  }
})()

proxyMult(1, 2, 3, 4)
proxyMult(1, 2, 3, 8)

console.log(proxyMult(1, 2, 3, 4)); // 第二次调用的时候本体没有参与计算,proxyMult直接返回了之前缓存的结果
console.log(proxyMult(1, 2, 3, 8));

5.利用高阶函数动态创建代理 

// 将各种计算方法当做参数传入创建缓存代理的工厂中
var mult = function () {
  var a = 1
  for (var i = 0; i < arguments.length; i++) {
    a = a * arguments[i]
  }
  return a
}

var plus = function () {
  var a = 0
  for (var i = 0; i < arguments.length; i++) {
    a = a + arguments[i]
  }
  return a
}
// 创建代理工厂 ,将算法函数当作参数传入
var createProxyFactory = function (fn) {
  var cache = {}
  return function () {
    var args = Array.prototype.join.call(arguments, ',')
    if (args in cache) { // 看看cache中有没有args这个属性
      return cache[args]
    }
    return cache[args] = fn.apply(this, arguments)
  }
}

var proxyMult = createProxyFactory(mult)
var proxyPlus = createProxyFactory(plus)
proxyMult(1, 2, 3, 4)

 补充知识:

  • 防火墙代理:控制网络资源的访问,保护主机不被坏人接近
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在java中可以是另一个虚拟机中的对象
  • 保护代理:用于对象应该有不同访问权限的情况
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数
  • 写时复制代理:通常用于复制一个庞大对象的情况,写时复制代理延迟了复制的过程,当对象被真正修改时,才对他进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是典型的运用场景

四、迭代器模式

今天聊点轻松的话题。稍微了解一下forEach原理。

1.实现自己的迭代器

var each = function (ary, callback) {
  for (var i = 0; i < ary.length; i++) {
    callback.call(ary[i], i, ary[i]);
  }
}

each(['hello', 'world'], function (i, n) {
  console.log([i, n]);
})

// n:循环遍历的每一项
// i:索引

2.内部迭代器和外部迭代器

2.1内部迭代器

内部迭代器使用方便,但是迭代规则被提前制定比如现在要对比两个数组是否完全相等不改写each函数,能入手的只有回调函数了。

判断两个数组是否相等

var compare = function (ary1, ary2) {
  if (ary1.length !== ary2.length) {
    throw new Error('不相等')
  }
  each(ary1, function (i, n) {
    if (n !== ary2[i]) {
      throw new Error('不相等')
    }
  })
  alert('相等')
}
compare([1, 2, 3], [1, 2, '3']) // 抛出错误:不相等

2.2外部迭代器

外部迭代器必须显式的请求迭代下一个元素,增加了一定的调用复杂度,但相对增强了灵活性

下面还是判断两个数组是否相等

// 松本行弘的Ruby迭代器思想
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
  }
}

// 改造compare函数
var compare = function (iterator1, iterator2) {
  if (iterator1.length !== iterator2.length) {
    throw new Error('不相等')
  }
  while (!iterator1.isDone() && !iterator2.isDone()) {
    if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
      throw new Error('不相等')
    }
    iterator1.next()
    iterator2.next()
  }
  alert('相等')
}

var iterator1 = Iterator([1, 2, 3, 4])
var iterator2 = Iterator([2, 2, 3, 4])

compare(iterator1, iterator2)

3.倒叙迭代器

var reverseEach = function (ary, cb) {
  for (var i = ary.length - 1; i >= 0; i--) {
    cb(i, ary[i]);
  }
}
reverseEach([0, 1, 12], function (i, n) {
  console.log(n);
})

//  附上冒泡排序
// function bubbleSort (arr) {
//   for (var i = 0; i < arr.length - 1; i++) {
//     for (var j = 0; j < arr.length - 1 - i; j++) {
//       if (arr[j] > arr[j + 1]) {
//         var temp = arr[j]
//         arr[j] = arr[j + 1]
//         arr[j + 1] = temp
//       }
//     }
//   }
//   return arr
// }

4.中止迭代器

var each = function (ary, cb) {
  for (var i = 0; i < ary.length; i++) {
    if (cb(i, ary[i]) === false) break; // cb的结果返回false 提前终止迭代
  }
}

each([2, 12, 3, 4, 8], function (i, n) {
  if (n > 5) return false;
  console.log(n);
})

5.在项目中优化的应用

场景描述:根据不同浏览器获取相应的上传组件对象

// 可以看到下面的代码充斥了try catch if else 缺点是显而易见的 
// 第一难以阅读 第二严重违反开放封闭原则
var getUploadObj = function () {
  try {
    return new ActiveXObject("TXFTNActiveX.FTNUpload") // 优先使用IE上传控件
  } catch (e) {
    if (supportFlash()) { // flash上传
      var str = '<object type="application/x-shockwave-flash"></object>';
      return $(str).appendTo($('body'))
    } else {
      var str = '<input name="file" type="file"></input>'; // 表单上传
      return $(str).appendTo($('body'))
    }
  }
}

// 利用迭代器模式优化 
// 共同约定如果有可用的则返回该对象 没有返回false通知迭代器继续迭代
var getActiveObj = function () {
  try {
    return new ActiveXObject("TXFTNActiveX.FTNUpload")
  } catch (e) {
    return false
  }
}

var getFlashObj = function () {
  if (supportFlash()) { // flash上传
    var str = '<object type="application/x-shockwave-flash"></object>';
    return $(str).appendTo($('body'))
  }
  return false
}

var getFormObj = function () {
  var str = '<input name="file" type="file"></input>'; // 表单上传
  return $(str).appendTo($('body'))
}

// 迭代器
var iteratorUploadObj = function () {
  for (var i = 0, fn; fn = arguments[i++];) {
    var uploadObj = fn()
    if (uploadObj !== false) {
      return uploadObj
    }
  }
}
var uploadObj = iteratorUploadObj(getActiveObj, getFlashObj, getFormObj)

五、发布订阅模式

重点来了,该模式不仅在上层应用中的逻辑频繁体现,而且包括Vue2底层的源码也是利用其思想实现(react中redux的一些方法也有发布订阅的影子)。该模式占据的篇幅较多,慢慢的啃,尝试着去理解。相信你一定会获益良多!

定义:又叫观察者模式,定义对象间的一对多的依赖关系,当一个对象发生改变时,所有依赖于他的对象都将得到通知

1. 模拟售楼部的发布订阅场景


var salesOffices = {} // 发布者(售楼部)
salesOffices.clientList = [] // 缓存列表(客户列表)

salesOffices.listen = function (fn) { // 监听订阅者订阅
  this.clientList.push(fn); // 添加缓存列表
}

salesOffices.trigger = function () { // 发布
  for (var i = 0, fn; fn = this.clientList[i++];) {
    fn.apply(this, arguments);
  }
}

// 浅测一下
salesOffices.listen(function (price, squareMeter) { // 小明订阅
  console.log('价格=' + price);
  console.log('平方米=' + squareMeter);
})

salesOffices.listen(function (price, squareMeter) { // 小红订阅
  console.log('价格=' + price);
  console.log('平方米=' + squareMeter);
})

salesOffices.trigger(20000000, 88) //价格=20000000,平方米=88 售楼部发布
salesOffices.trigger(30000000, 110) // 售楼部发布

可以看到,小明只想买88平米的房子的价格,但是售楼mm吧110的价格也推给了小明 造成了不必要的困扰,所以有必要增加一个标识key,让订阅者只接受他们感兴趣的信息。如下。

// 模拟售楼的观察者模式
var salesOffices = {} // 发布者(售楼部)
salesOffices.clientList = {} // 缓存列表(客户列表)

salesOffices.listen = function (key, fn) { // 监听订阅者订阅
  if (!this.clientList[key]) { // 如果没有订阅过此类消息,给该类创建缓存列表
    this.clientList[key] = []
  }
  this.clientList[key].push(fn); // 给对应的订阅者添加缓存列表
}

salesOffices.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);
  }
}

// 浅测一下
salesOffices.listen('88平米', function (price) { // 小明订阅
  console.log('价格=' + price);
})

salesOffices.listen('110平米', function (price) { // 小红订阅
  console.log('价格=' + price);
})

salesOffices.trigger('88平米', 20000000) //价格=20000000,squareMeter=88
salesOffices.trigger('110平米', 30000000) 

2. 发布订阅的通用实现

// 提取发布订阅功能
var event = {
  clientList: {},
  listen: function (key, fn) { // 监听订阅
    if (!this.clientList[key]) {
      this.clientList[key] = []; // 没有订阅过创建订阅队列
    }
    this.clientList[key].push(fn);
  },

  trigger: function () { // 发布订阅
    var key = Array.prototype.shift.call(arguments), // arguments 是fn函数附带的参数
      fns = this.clientList[key];
    if (!fns || !fns.length === 0) {
      return false
    }
    for (var i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments);
    }
  },

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

// 定义一个installEvent函数,给所有对象动态安装发布订阅模式
var installEvent = function (obj) {
  for (var key in event) {
    obj[key] = event[key]
  }
}

// 测试
var salesOffices = {}
installEvent(salesOffices) // 安装
salesOffices.listen('我要订阅88平米的', fn1 = function (price) { // 小明订阅
  console.log('价格=' + price);
})
salesOffices.listen('我要订阅88平米的', fn2 = function (price) { // 小红订阅
  console.log('价格=' + price);
})
salesOffices.remove('我要订阅88平米的', fn1) // 移除小明的订阅
salesOffices.trigger('我要订阅88平米的', 30000000) 

3. 真实例子-网站登录

// 强耦合的情况
login.succ(function (data) {
  header.setAvatar(data.avatar) // 设置用户头像
  nav.setAvatar(data.avatar) // 设置用户信息
  message.refresh() // 刷新消息列表
})

// 观察者模式
// 发布登陆成功的消息,不用关心订阅者干什么
$.ajax('http://xxx.xxx.xxx.xxx', function (data) {
  login.trigger('loginSucc', data)
})

// 各模块监听登陆成功的消息
var header = (function () {
  login.listen('loginSucc', function (data) {
    header.setAvatar(datta.avatar)
  })
})()
// 后面的依次类推

4. 全局的发布订阅模式与模块间的通信

注意:模块间用了太多的全局发布订阅,那么他们的联系就被隐藏到了背后,最终会搞不清楚流向哪个模块或者消息来自何方

如下,在通用发布订阅中使用了售楼部案例又使用了count++案例

<body>
  <div id="show" style="width: 50px; height: 50px; background-color: bisque;"></div>
  <button id="count">点我+1</button>

  <script>
    var Event = (function () {
      var clientList = {},
        listen,
        trigger,
        remove
      listen = function (key, fn) {
        if (!clientList[key]) {
          clientList[key] = []
        }
        clientList[key].push(fn)
      }
      trigger = function () {
        var key = Array.prototype.shift.call(arguments),
          fns = clientList[key]
        if (!fns || fns.length === 0) return false
        for (var i = 0, fn; fn = fns[i++];) {
          fn.apply(this, arguments)
        }
      }
      remove = function (key, fn) {
        var fns = clientList[key]
        if (!fns) return false
        if (!fn) {
          fns && (fns.length = 0)
        } else {
          for (var i = fns.length - 1; i >= 0; i--) {
            var _fn = fns[i]
            if (_fn === fn) {
              fns.splice(i, 1)
            }
          }
        }
      }
      return {
        clientList,
        listen,
        trigger,
        remove
      }
    })()

    Event.listen('88平米', function (price) {
      console.log('价格' + price);
    })
    Event.trigger('88平米', 200000)

    var count = 0
    var a = (function () {
      // var count = 0
      var btn = document.getElementById("count")
      btn.onclick = function () {
        Event.trigger('add', ++count)
      }
    })()

    var b = (function () {
      var div = document.getElementById("show")
      div.innerHTML = count
      Event.listen('add', function (count) {
        div.innerText = count
      })
    })()
  </script>
</body>

vue2源码对此做了优化,采用多对多的形式,有效解决了该问题

六、命令模式

命令模式常见的场景是有时候需要向某些对象发送请求,但是并不知道请求的接受者是谁,也不知道请求的操作是什么。此时希望用一种松耦合的方式设计程序,使得请求发送者和请求接受者能够消除彼此之间的耦合关系。就像顾客点餐不用关心菜是那个厨师炒,厨师也不用管谁订餐,可以选择什么时间段炒,也可以取消订单。

1. JavaScript中的命令模式

// 定义一些按钮去执行一些事情
var bindClick = function (button, func) {
  button.onclick = func;
}
var MenuBar = {
  refresh: function () {
    console.log('刷新菜单');
  }
}
var SubMenu = {
  add: function () {
    console.log('增加子菜单');
  },
  del: function () {
    console.log('删除子菜单');
  }
}
bindClick(button1, MenuBar.refresh())

用闭包实现(松耦合)

var setCommand = function (button, command) {
  button.onClick = function () {
    command.execute()
  }
}
var MenuBar = {
  refresh: function () {
    console.log('刷新菜单');
  }
}
var RefreshMenuBarCommand = function (receiver) {
  return {
    execute: function () {
      receiver.refresh()
    }
  }
}
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
setCommand(button1, refreshMenuBarCommand)

注意:此处引入了execute的概念,相当于命令的开关 

2. 撤销和重做

利用街头霸王回放案例

<body>
  <button id="replay">播放录像</button>
  <script>
    var Ryu = {
      attack: function () {
        console.log('攻击');
      },
      defense: function () {
        console.log('防御');
      },
      jump: function () {
        console.log('跳跃');
      },
      crouch: function () {
        console.log('蹲下');
      }
    }

    var makeCommand = function (receiver, state) {
      return function () {
        receiver[state]()
      }
    }

    var commands = {
      '119': 'jump', // w
      '115': 'crouch', // s
      '97': 'defence', // a
      '100': 'attack' //d
    }

    var commandStack = []
    document.onkeypress = function (ev) {
      var keyCode = ev.keyCode,
        command = makeCommand(Ryu, commands[keyCode])

      if (command) {
        command() // 执行命令
        commandStack.push(command) // 将刚刚执行过的命令保存到堆栈
      }
    }
    document.getElementById('replay').onclick = function () { // 点击播放录像
      var command
      while (command = commandStack.shift()) { // 从堆栈取出命令并执行
        command()
      }
    }
  </script>
</body>

 注意:上述代码引入了commandStack缓存队列的概念,此处可以理解为等待执行的命令队列

3. 命令队列

将执行的命令封装成命令对象,压进一个队列堆栈。当前command对象职责完成后,主动通知队列,取出队列中等待的第一个命令对象,并且执行他。可以用回调函数通知,也可以用发布订阅。即command对象结束后发布一个消息,订阅者收到消息后,执行队列第一个。

4. 宏命令(下一章节将为大家介绍更高级的宏命令,此处仅为引申帮大家了解命令队列的执行机制)

假设我们结束了一天疲惫的工作回到家后要做的3件事情,关门,开电脑,登QQ,此时可以利用上述思想将这3件事情封装成宏命令的模式。

var closeDoorCommand = {
  execute: function () {
    console.log('关门');
  }
}

var openPcCommand = {
  execute: function () {
    console.log('开电脑');
  }
}

var openQQCommand = {
  execute: function () {
    console.log('登qq');
  }
}
// macroCommand.add 把子命令添加进宏命令对象,
// 当调用宏命令对象的execute方法时,迭代这一组子命令对象,并依次执行它们的execute方法
var MacroCommand = function () {
  return {
    commandList: [], // 命令队列
    add: function (command) {
      // console.log(command);
      this.commandList.push(command);
    },
    execute: function () {
      for (var i = 0, command; command = this.commandList[i++];) {
        command.execute()
      }
    }
  }
}
var macroCommand = MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute

七、组合模式

定义:组合模式就是由小的子对象来构建更大的对象,而这些小的子对象也许是由更小的孙对象构成的。构建树完成后只需要请求树的最顶层对象,遍能对整棵树做统一的操作。符合开放封闭原则。客户不用关心当前处理的对象是组合对象还是叶对象,也就不用写一大堆if else分别处理他们,组合对象和叶对象会各自做自己正确的事 这是组合模式最重要的能力。

弊端:代码看起来都是一样的 只有运行的时候才能看出区别 不利于理解 。如果利用组合模式创建太多对象的时候 会让系统负担不起

1. 更强大的宏命令 

还是拿上面的关门开电脑登QQ为例,现在加入了新动作:开空调、开电视、开音响。我们一起来看一下超级命令是如何实现的。

<body>
  <button id="btn">宏命令</button>
  <script>
    var MacroCommand = function () {
      return {
        commandsList: [],
        add: function (command) {
          this.commandsList.push(command);
        },
        execute: function () {
          for (var i = 0, command; command = this.commandsList[i++];) {
            command.execute()
          }
        }
      }
    }

    var openAcCommand = {
      execute: () => console.log('打开空调')
    }
    var openTvCommand = {
      execute: () => console.log('打开电视')
    }
    var openSoundsCommand = {
      execute: () => console.log('打开音响')
    }
    // 电视和音响是一个宏命令
    var macroCommand1 = MacroCommand()
    macroCommand1.add(openTvCommand)
    macroCommand1.add(openSoundsCommand)

    var closeDoorCommand = {
      execute: function () {
        console.log('关门');
      }
    }
    var openPcCommand = {
      execute: function () {
        console.log('开电脑');
      }
    }
    var openQQCommand = {
      execute: function () {
        console.log('登qq');
      }
    }
    // 关门 开电脑 登qq是一个宏命令
    var macroCommand2 = MacroCommand()
    macroCommand2.add(closeDoorCommand)
    macroCommand2.add(openPcCommand)
    macroCommand2.add(openQQCommand)

    // 把所有的命令组合成超级命令
    var macroCommand = MacroCommand()
    macroCommand.add(openAcCommand) // 开空调
    macroCommand.add(macroCommand1) // 开音响 + 开电视
    macroCommand.add(macroCommand2) // 关门 + 开电脑 + 登QQ

    // 绑定超级命令
    var setCommand = (command => {
      document.getElementById('btn').onclick = () => {
        command.execute()
      }
    })(macroCommand)
  </script>
</body>

2. 模仿扫描文件的组合模式

    // 文件夹类
    var Folder = function (name) {
      this.name = name // 文件名
      this.parent = null // 根
      this.files = [] // 文件列表
    }
    Folder.prototype.add = function (file) {
      file.parent = this // 设置父对象
      this.files.push(file)
    }
    Folder.prototype.scan = function () {
      console.log('开始扫描文件夹:' + this.name);
      for (var i = 0, file, files = this.files; file = files[i++];) {
        file.scan()
      }
    }
    Folder.prototype.remove = function () {
      if (!this.parent) return // 根节点或者树外游离节点
      for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
        var file = files[l]
        if (file === this) {
          files.splice(l, 1)
        }
      }
    }

    // 文件类
    var File = function (name) {
      this.name = name
      this.parent = null
    }
    File.prototype.add = function () {
      throw new Error('不能添加在文件下面')
    }
    File.prototype.scan = function () {
      console.log('开始扫描文件:' + this.name);
    }
    File.prototype.remove = function () {
      if (!this.parent) return // 根节点或者树外游离节点
      for (var files = this.parent.files, l = files.length - 1; l >= 0; l--) {
        var file = files[l]
        if (file === this) {
          files.splice(l, 1)
        }
      }
    }

    var folder = new Folder('学习资料')
    var folder1 = new Folder('javascript')
    var file = new File('深入浅出node.js')

    folder1.add(new File('js开发与实践')) // javascript/js开发与实践
    folder1.add(file) // javascript/js开发与实践/深入浅出node.js
    folder.add(folder1) //学习资料/javascript/js开发与实践/深入浅出node.js

    folder1.remove() // javascript/js开发与实践/深入浅出node.js
    folder.scan() // 开始扫描文件夹: 学习资料

八、模板方法模式

定义:只需用继承就可实现的简单的模式。第一部分是抽象父类,第二部分是具体实现子类。父类封装了子类的一些算法框架,包括公共方法和子类所有方法的执行顺序,子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

 1. 泡咖啡、泡茶的例子

   // 泡咖啡
    // 把水煮沸
    // 用沸水冲泡咖啡
    // 把咖啡倒进杯子
    // 加糖和牛奶

    var Coffee = function () { }
    Coffee.prototype.boilWater = function () {
      console.log('水煮沸');
    }
    Coffee.prototype.brewCoffGriends = function () {
      console.log('用沸水冲泡咖啡');
    }
    Coffee.prototype.pourInCup = function () {
      console.log('把咖啡倒进杯子');
    }
    Coffee.prototype.addSugarAndMilk = function () {
      console.log('加糖和牛奶');
    }
    Coffee.prototype.init = function () {
      this.boilWater()
      this.brewCoffGriends()
      this.pourInCup()
      this.addSugarAndMilk()
    }

    var coffee = new Coffee()
    coffee.init()

    // 泡茶
    // 把水煮沸
    // 用沸水浸泡茶叶
    // 茶水倒进杯子
    // 加柠檬
    // 代码略

    // 不同点 原料 可以都抽象为饮料 泡的方式 抽象为泡 加入调料不同 抽象为调料
// 把水煮沸
// 用沸水泡咖啡
// 饮料倒进杯子
// 加调料

2. 用模板方法构建(附带钩子函数)

    // 饮料抽象类
    var Beverage = function () { }
    Beverage.prototype.boilWater = function () { // 公共方法
      console.log('把水煮沸');
    }
    Beverage.prototype.brew = function () { } // 空方法 应该子类编写
    Beverage.prototype.pourInCup = function () { } // 倒进杯子
    Beverage.prototype.addCondiments = function () { } // 倒调料
    Beverage.prototype.customerWantsCondiments = function () { return true } // 挂载钩子函数 默认需要调料
    Beverage.prototype.init = function () { // 模板方法 封装子类的算法框架 指导子类以何种顺序执行哪些方法
      this.boilWater()
      this.brew() // 调用子类重写的方法
      this.pourInCup()
      if (this.customerWantsCondiments()) {
        this.addCondiments()
      }
    }

    // 创建咖啡子类
    var Coffee = function () { }
    Coffee.prototype = new Beverage()

    Coffee.prototype.brew = function () {
      console.log('用沸水冲泡咖啡');
    }
    Coffee.prototype.pourInCup = function () {
      console.log('把咖啡倒进杯子');
    }
    Coffee.prototype.addCondiments = function () {
      console.log('加糖和牛奶');
    }
    Coffee.prototype.customerWantsCondiments = function () {
      return window.confirm('需要调料吗')
    }
    var coffee = new Coffee()
    coffee.init() //coffee 对象和构造函数没有init 请求会顺着原型链委托给父类Beverage原型上的init方法

    // 泡茶代码略

3.高阶函数是更好的选择(比上面的代码快0.1ms)

    var Beverage = function (params) {
      var boilWater = function () {
        console.log('Boil the water');
      }
      var brew = params.brew || function () {
        throw new Error('The method must be passed')
      }
      var pourInCup = params.pourInCup || function () {
        throw new Error('The method must be passed')
      }
      var addCondiments = params.addCondiments || function () {
        throw new Error('The method must be passed')
      }
      var F = function () { }
      F.prototype.init = function () {
        boilWater()
        brew()
        pourInCup()
        addCondiments()
      }
      return F
    }

    var Coffee = Beverage({
      brew: () => console.log('Brew coffee with boiling water'),
      pourInCup: () => console.log('Pour the coffee into the cup'),
      addCondiments: () => console.log('Add sugar and milk')
    })
    var coffee = new Coffee()
    coffee.init()

九、享元模式

一种用于性能优化的模式。运用共享技术有效支持大量细粒度的对象。系统由于创建大量类似的对象而导致内存占用过高,特别是移动端浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事。

享元模式要求将对象属性划分成内部状态和外部状态(状态在这里通常指属性)目标是尽量减少共享对象的数量。

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

剥离了外部状态的对象成为共享对象,外部状态在必要时被传人共享对象来组裝成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。

 1. 腾讯微云上传案例(未优化)

var id = 0

window.startUpload = function (type, files) {
  for (var i = 0, file; file = files[i++];) { // 传多少个文件创建多少个对象
    var uploadObj = new Upload(type, file.name, file.size)
    uploadObj.init(id++)
  }
}

var Upload = function (type, name, size) {
  this.type = type
  this.name = name
  this.size = size
  this.dom = null
}

Upload.prototype.init = function (id) {
  var _this = this
  this.id = id
  this.dom = document.createElement('div')
  this.dom.innerHTML = `<span>文件名:${this.name},文件大小:${this.size}</span><button class="delFile">删除</button>`
  this.dom.querySelector('.delFile').onclick = function () {
    _this.delFile()
  }
  document.body.appendChild(this.dom)
}

Upload.prototype.delFile = function () {
  if (this.size < 3000) {
    return this.dom.parentNode.removeChild(this.dom)
  }
  if (window.confirm('Are you sure you want to delete ' + this.name)) {
    return this.dom.parentNode.removeChild(this.dom)
  }
}

startUpload('plugin', [
  {
    name: '1.txt',
    size: 4000,
  },
  {
    name: '2.txt',
    size: 1000
  }
])

2. 通过享元模式优化(之前会创建多个对象 现在同时上传2000个也只创建两个对象)

注意:下面代码引入了对象池的理念


var Upload = function (type) { // 内部状态
  this.type = type
}

Upload.prototype.delFile = function (id) {
  // 开始删除之前,需要读取文件实际大小,实际大小被存储在外部管理器uploadManager中,通过这个方法给共享对象设置正确的size
  // 把当前id对应的对象的外部状态组装到共享对象中
  uploadManager.setExternalState(id, this)

  if (this.size < 3000) {
    return this.dom.parentNode.removeChild(this.dom)
  }
  if (window.confirm('Are you sure you want to delete ' + this.name)) {
    return this.dom.parentNode.removeChild(this.dom)
  }
}

// 创建工厂对象,如果内部状态对应的共享对象已经创建过 直接返回 否则创建新对象
var uploadFactory = (function () {
  var createdFlyWeightObjs = {}
  return {
    create: function (type) {
      if (createdFlyWeightObjs[type]) return createdFlyWeightObjs[type]
      return createdFlyWeightObjs[type] = new Upload(type)
    }
  }
})()

// 提交创建对象的请求
var uploadManager = (function () {
  var uploadDatabase = {} // 保存所有upload外部状态
  return {
    add: function (id, type, name, size) {
      var flyWeightObj = uploadFactory.create(type) // 去工厂函数验证是否被创建过
      var dom = document.createElement("div")
      dom.innerHTML = `<span>文件名:${name},文件大小:${size}</span><button class="delFile">删除</button>`
      dom.querySelector('.delFile').onclick = function () { // 绑定删除的方法
        flyWeightObj.delFile(id)
      }
      document.body.appendChild(dom)
      uploadDatabase[id] = {
        name,
        size,
        dom
      }
      return flyWeightObj
    },
    setExternalState: function (id, flyWeightObj) {
      var uploadData = uploadDatabase[id]
      for (var i in uploadData) {
        flyWeightObj[i] = uploadData[i]
      }
    }
  }
})()

var id = 0

window.startUpload = function (type, files) {
  for (var i = 0, file; file = files[i++];) { // 传多少个文件创建多少个对象
    var uploadObj = uploadManager.add(++id, type, file.name, file.size)
  }
}

startUpload('plugin', [
  {
    name: '1.txt',
    size: 4000,
  },
  {
    name: '2.txt',
    size: 1000
  }
])

3. 对象池

var objectPoolFactory = function (createObjFn) {
  var objPool = []

  return {
    create: function () {
      var obj = objPool.length === 0 ? createObjFn.apply(this, arguments) : objPool.shift()
      return obj
    },
    recover: function (obj) {
      objPool.push(obj)
    }
  } 
}

// 创建一个装载一些iframe的对象池
var iframeFactory = objectPoolFactory(function () {
  var iframe = document.createElement('iframe')
  document.body.appendChild(iframe)

  iframe.onload = function () {
    iframe.onload = null // 防止重复加载
    iframeFactory.recover(iframe) // 加载成功后回收该节点
  }
  return iframe
})

// 测试
var iframe1 = iframeFactory.create()
iframe1.src = 'http://hao123.com'

var iframe2 = iframeFactory.create()
iframe2.src = 'https://4399.com'

setTimeout(() => {
  var iframe3 = iframeFactory.create()
  iframe3.src = 'https://163.com'
}, 3000)

十、职责链模式

定义:使多个对象都有机会处理请求,从而避免请求者跟接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链依次传递请求,直到有一个对象处理它为止. eventLoop事件轮询

1. 模拟商品订单处理 

    // orderType === 1:500定金用户 ,2:200定金用户, 3:普通用户
    // pay 是否交了定金
    // stock 库存(定金用户不受限制)
    var order500 = function (orderType, pay, stock) {
      if (orderType === 1 && pay) {
        console.log('得到100优惠券');
      } else {
        order200(orderType, pay, stock); // 传递给200定金的函数处理
      }
    }

    var order200 = function (orderType, pay, stock) {
      if (orderType === 2 && pay) {
        console.log('得到50元优惠券');
      } else {
        orderNormal(orderType, pay, stock);
      }
    }

    var orderNormal = function (orderType, pay, stock) {
      if (stock > 0) {
        console.log('普通购买');
      } else {
        console.log('库存不足');
      }
    }

    order500(1, false, 1000)

2. 灵活可拆分的职责链节点

var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay) {
    console.log('得到100优惠券');
  } else {
    return 'nextSuccessor' // 不知道下一个节点是谁 反正往后面传递
  }
}

var order200 = function (orderType, pay, stock) {
  if (orderType === 2 && pay) {
    console.log('得到50元优惠券');
  } else {
    return 'nextSuccessor' // 不知道下一个节点是谁 反正往后面传递
  }
}

var orderNormal = function (orderType, pay, stock) {
  if (stock > 0) {
    console.log('普通购买');
  } else {
    console.log('库存不足');
  }
}

// 构造函数 new的时候传递的参数为需要被包装的函数, 拥有一个实例属性this.successor表示链中的下一个节点
var Chain = function (fn) {
  this.fn = fn;
  this.successor = null;
}
Chain.prototype.setNextSuccessor = function (successor) { // 指定链中的下一个节点
  return this.successor = successor
}
Chain.prototype.passRequest = function () { // 传递请求给某个节点
  var ret = this.fn.apply(this, arguments) // 先屌用这个函数
  if (ret === 'nextSuccessor') { // 如果处理不了 直接指定下一个节点 并且 将传递的方法也交给下一个函数
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
  return ret
}

// 1.把上面3个函数包装成职责链节点
var chainOrder500 = new Chain(order500)
var chainOrder200 = new Chain(order200)
var chainOrderNormal = new Chain(orderNormal)

// 2.然后指定节点在职责链中的顺序
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

// 3.最后把请求传递给第一个节点
chainOrder500.passRequest(1, false, 500)

// 如果支持300定金直接增加
// var order300 = function() {
//   // 代码略
// }
// chainOrder300 = new Chain(order300)
// chainOrder500.setNextSuccessor(chainOrder300)
// ...

// 异步职责链 添加next方法 this.手动调用
Chain.prototype.next = function () {
  return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}

3. 用AOP实现职责链(相当的优雅)

Function.prototype.after = function (fn) {
  var _this = this;
  return function () {
    var ret = _this.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优惠券

十一、中介者模式

定义:面向对象设计鼓励将行为分不到各个对象中,划分成更小的粒度,有助于增强对象的可复用性.但由于这些细粒度对象联系激增,又可能反过来降低可复用性.中介者模式作用就是接触对象间的紧耦合关系,所有相关对象通过中介者对象通信,而不是相互引用.

中介者使网状的多对多关系变成了相对简单的一对多关系.

1. 泡泡堂案例 --双人对战

function Player (name) {
  this.name = name;
  this.enemy = null // 敌人
}

Player.prototype.win = function () {
  console.log(this.name + "won");
}
Player.prototype.lose = function () {
  console.log(this.name + 'lost');
}
Player.prototype.die = function () {
  this.lose()
  this.enemy.win()
}

var player1 = new Player('皮蛋')
var player2 = new Player('小乖')

// 彼此是对方的敌人
player1.enemy = player2
player2.enemy = player1

player1.die()

2. 泡泡堂案例 --多人对战

var players = []  // 所有玩家队列

function Player (name, teamColor) {
  this.partners = []; // 队友列表
  this.enemies = []; // 敌人列表
  this.name = name;
  this.teamColor = teamColor;
}

Player.prototype.win = function () {
  console.log('winner' + this.name);
}
Player.prototype.lose = function () {
  console.log('loser' + this.name);
}

// 玩家死亡遍历队友生存情况 ,如果队友全部死亡 游戏失败,敌人队伍所有玩家取得胜利 
Player.prototype.die = function () {
  var all_dead = true
  this.state = 'dead' // 设置玩家状态为死亡
  for (var i = 0, partner; partner = this.partners[i++];) {
    if (partner.state !== 'dead') { // 如果还有一个队友没有死亡,则还没有失败
      all_dead = false
      break
    }
  }
  if (all_dead === true) {
    this.lose() // 队伍失败
    for (var i = 0, partner; partner = this.partners[i++];) {
      partner.lose() // 通知所有队伍成员失败
    }
    for (var i = 0, enemy; enemy = this.enemies[i++];) {
      enemy.win() // 通知所有敌人胜利
    }
  }
}

// 定义一个工厂创建玩家
var PlayerFactory = function (name, teamColor) {
  var newPlayer = new Player(name, teamColor)
  for (var i = 0, player; player = players[i++];) { // 通知所有玩家有新玩家加入
    if (player.teamColor === newPlayer.teamColor) {
      player.partners.push(newPlayer) // 队友互相添加
      newPlayer.partners.push(player) // 更新至最新玩家列表
    } else {
      player.enemies.push(newPlayer)
      newPlayer.enemies.push(player)
    }
  }
  players.push(newPlayer)
  return newPlayer
}

var player1 = PlayerFactory('没头脑', 'red')
var player2 = PlayerFactory('不高兴', 'red')

var palyer3 = PlayerFactory('大聪明', 'blue')
var palyer4 = PlayerFactory('大明白', 'blue')

player1.die()
player2.die()

// loser不高兴
// loser没头脑
// winner大聪明
// winner大明白

3. 泡泡堂案例 --引入中介者模式实现更多功能

var players = []

function Player (name, teamColor) {
  this.name = name;
  this.teamColor = teamColor;
  this.state = 'alive'
}

Player.prototype.win = function () {
  console.log('winner' + this.name);
}
Player.prototype.lose = function () {
  console.log('loser' + this.name);
}

// 玩家死亡 给中介者发消息
Player.prototype.die = function () {
  this.state = 'dead'
  playerDirector.ReceiveMessage('playerDead', this)
}
// 移除玩家
Player.prototype.remove = function () {
  playerDirector.ReceiveMessage('removePlayer', this)

}

// 玩家换队
Player.prototype.changeTeam = function (color) {
  playerDirector.ReceiveMessage('changeTeam', this, color)
}

// 定义一个工厂创建玩家
var PlayerFactory = function (name, teamColor) {
  var newPlayer = new Player(name, teamColor)
  playerDirector.ReceiveMessage('addPlayer', newPlayer)
  return newPlayer
}

// 中介者
var playerDirector = (function () {
  var players = {}, // 保存所有玩家
    operations = {} // 中介者可执行的操作
  // 新增玩家
  operations.addPlayer = function (player) {
    var teamColor = player.teamColor
    players[teamColor] = players[teamColor] || [] // 如果该颜色玩家没有成立队伍,则新成立一个队伍
    players[teamColor].push(player) // 加入队伍
  }
  // 移除玩家
  operations.removePlayer = function (player) {
    var teamColor = player.teamColor, // 玩家队伍颜色
      teamPlayers = players[teamColor] || [] // 该队伍所有成员
    for (var i = teamPlayers.length - 1; i >= 0; i--) {
      if (teamPlayers[i] === player) {
        teamPlayers.splice(i, 1)
      }
    }
  }
  // 玩家换队
  operations.changeTeam = function (player, newteamColor) {
    operations.removePlayer(player)
    player.teamColor = newteamColor
    operations.addPlayer(player)
  }
  // 玩家死亡
  operations.playerDead = function (player) {
    var teamColor = player.teamColor
    teamPlayers = players[teamColor]
    var allDead = true
    for (var i = 0, player; player = teamPlayers[i++];) {
      if (player.state !== 'dead') {
        allDead = false
        break
      }
    }
    if (allDead) {
      for (var i = 0, player; player = teamPlayers[i++];) {
        player.lose()
      }
      for (var color in players) {
        if (color !== teamColor) {
          var teamPlayers = players[color] // 其他队伍的玩家
          for (var i = 0, player; player = teamPlayers[i++];) {
            player.win()
          }
        }
      }
    }
  }
  var ReceiveMessage = function () {
    var message = Array.prototype.shift.call(arguments) // arguments的第一个参数是消息名称
    operations[message].apply(this, arguments)
  }
  return {
    ReceiveMessage
  }
})()

var player1 = PlayerFactory('没头脑', 'red')
var player2 = PlayerFactory('不高兴', 'red')
var player250 = PlayerFactory('庄强', 'red')

var palyer3 = PlayerFactory('大聪明', 'blue')
var palyer4 = PlayerFactory('大明白', 'blue')
var player5 = PlayerFactory('黄策', 'blue')

player250.changeTeam('blue')
player1.die()
player2.die()

4.购买商品案例

<body>
  <label for="colorSelect">
    选择颜色:
    <select name="" id="colorSelect">
      <option value="">请选择</option>
      <option value="red">红色</option>
      <option value="blue">蓝色</option>
    </select>
  </label>

  <label for="numberInput">
    输入购买数量:
    <input type="text" name="" id="numberInput">
  </label>

  <div>
    您选择了颜色:<div id="colorInfo"></div>
    您输入了数量:<div id="numberInfo"></div>
  </div>

  <button id="nextBtn" disabled>请选择颜色和购买数量</button>

  <script>

    var colorSelect = document.getElementById("colorSelect"),
      numberInput = document.getElementById("numberInput"),
      colorInfo = document.getElementById("colorInfo"),
      numberInfo = document.getElementById("numberInfo"),
      nextBtn = document.getElementById("nextBtn")
    var goods = {
      "red": 3,
      "blue": 6
    }
    //    var goods = {
    //   "red|32G": 3,
    //   "red|16G": 0,
    //   "blue|32G": 1,
    //   "blue|16G": 6
    // }

    colorSelect.onchange = function () {
      var color = this.value,
        number = numberInput.value,
        stock = goods[color]
         // stock = goods[color + '|' + memory]

      colorInfo.innerHTML = color

      if (!color) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '请选择手机颜色'
        return
      }
          // if (!memory) {
       //      nextBtn.disabled = true
       //      nextBtn.innerHTML = '请选择内存大小'
       //      return
       //    }
      // 输入的数量是否为正整数
      if (Number.isInteger(number - 0) && number > 0) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '请输入正确的数量'
        return
      }
      if (number > stock) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '库存不足'
        return
      }
      nextBtn.disabled = false
      nextBtn.innerHTML = '加入购物车'

    }

    numberInput.oninput = function () {
      var color = colorSelect.value,
        number = this.value,
        stock = goods[color]
        // stock = goods[color + '|' + memory]

      numberInfo.innerHTML = number

      if (!color) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '请选择手机颜色'
        return
      }
       // if (!memory) {
       //      nextBtn.disabled = true
       //      nextBtn.innerHTML = '请选择内存大小'
       //      return
       //    }

      if (((number - 0) | 0) !== number - 0) { // 是否为正整数
        nextBtn.disabled = true
        nextBtn.innerHTML = '请输入正确的数量'
        return
      }
      if (number > stock) {
        nextBtn.disabled = true
        nextBtn.innerHTML = '库存不足'
        return
      }
      nextBtn.disabled = false
      nextBtn.innerHTML = '加入购物车'

    }

    // 注意:⚠️此处如不引入中介者,如加入一个内存的属性,将会出现大量重复耦合的代码
    // 结合上下文可以看到我注释的部分:首先在colorSelect和numberInput的事件函数加入内存的校验
    // 还得再写一套内存下拉框的事件函数
  </script>
</body>

以下,通过引入中介者模式改造


<body>
  <label for="colorSelect">
    选择颜色:
    <select name="" id="colorSelect">
      <option value="">请选择</option>
      <option value="red">红色</option>
      <option value="blue">蓝色</option>
    </select>
  </label>
  <label for="memorySelect">
    选择内存:
    <select name="" id="memorySelect">
      <option value="">请选择</option>
      <option value="32G">32G</option>
      <option value="16G">16G</option>
    </select>
  </label>



  <label for="numberInput">
    输入购买数量:
    <input type="text" name="" id="numberInput">
  </label>

  <div>
    您选择了颜色:<div id="colorInfo"></div>
    您输入了数量:<div id="numberInfo"></div>
    您选择了内存:<div id="memoryInfo"></div>
  </div>

  <button id="nextBtn" disabled>请选择颜色和购买数量</button>

  <script>


    var goods = {
      "red|32G": 3,
      "red|16G": 0,
      "blue|32G": 1,
      "blue|16G": 6
    }

    //   var goods = {
    //   "red|32G|800": 3, cpu800
    //   "red|16G|801": 0,
    //   "blue|32G|804": 1,
    //   "blue|16G|802": 6
    // }


    var mediator = (function () {
      var colorSelect = document.getElementById("colorSelect"),
        memorySelect = document.getElementById("memorySelect"),
        memoryInfo = document.getElementById("memoryInfo"),
        numberInput = document.getElementById("numberInput"),
        colorInfo = document.getElementById("colorInfo"),
        numberInfo = document.getElementById("numberInfo"),
        nextBtn = document.getElementById("nextBtn")
      // cpuSelect = document.getElementById("cpuSelect")
      return {
        changed: function (obj) {
          var color = colorSelect.value,
            memory = memorySelect.value,
            number = numberInput.value,
            stock = goods[color + '|' + memory]
          // cpu = cpuSelect.value
          // stock = goods[color + '|' + memory + '|' + cpu]

          if (obj === colorSelect) { // 如果改变的是选择颜色下拉框
            colorInfo.innerHTML = color
          } else if (obj === memorySelect) {
            memoryInfo.innerHTML = memory
          } else if (obj === numberInput) {
            numberInfo.innerHTML = number
          }
          // if(obj === cpuSelect) {
          //   cpuInfo.innerHTML = cpu
          // }

          if (!color) {
            nextBtn.disabled = true
            nextBtn.innerHTML = '请选择手机颜色'
            return
          }
          if (Number.isInteger(number - 0) && number > 0) {
            nextBtn.disabled = true
            nextBtn.innerHTML = '请输入正确的数量'
            return
          }

          if (!memory) {
            nextBtn.disabled = true
            nextBtn.innerHTML = '请选择内存大小'
            return
          }
          nextBtn.disabled = false
          nextBtn.innerHTML = '加入购物车'
        }
      }
    })()

    // 事件函数

    colorSelect.onchange = function () {
      mediator.changed(this)
    }

    memorySelect.onchange = function () {
      mediator.changed(this)
    }

    numberInput.onchange = function () {
      mediator.changed(this)
    }

    // 将来如果加入cpu的控制 见文中注释,可以看到已经解耦相当一部分了
  </script>

</body>

总结:中介者模式是迪米特法则的一种实现(最少知识原则,类似不和陌生人说话)在中介者模式里,对象间几乎不知道彼此的存在,只能通过中介舍来影响对方。

 十二、装饰者模式

不改变原有的对象的基础上,在程序运行期间给对象动态的添加职责。react-redux的dispatch处理异步的方式正是用了其思想

1. 利用AOP方式实现 

<button id="button">按钮</button>
<script>
  // 前置装饰
  Function.prototype.before = function (beforeFn) {
    var _self = this; // 保存原函数的引用
    console.log(_self); // ƒ getElementById()
    return function () {
      // 返回包含原函数和新函数的代理函数
      beforeFn.apply(_self, arguments); // 在原函数之前执行新函数,保证this不被劫持,新函数的参数也会原封不动的传给原函数
      console.log(this); // #document
      return _self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,保证this,不被劫持
    };
  };

  // 测试功能
  document.getElementById = document.getElementById.before(function () {
    alert(1);
  });
  var button = document.getElementById("button");
  console.log(button);

  // 同上 只是在执行原函数之后执行新函数
  Function.prototype.after = function (afterFn) {
    var _self = this;
    return function () {
      var ret = _self.apply(this, arguments);
      afterFn.apply(this, arguments);
      return ret;
    };
  };
</script>

值得一提:很多人不喜欢以上污染原型的方式,那么可以改成将原函数跟新函数一起当作参数传入。如下:

var before = function (fn, beforFn) {
  return function () {
    beforFn.apply(this, arguments);
    return fn.apply(this, arguments);
  };
};

var a = before(
  () => alert(3),
  () => alert(2)
);

a = before(a, () => alert(1));

a(); // 输出1 2 3

2. AOP的使用案例

<button tag="login" id="button">提交</button>
<script>
  // var showLogin = function () {
  //   console.log("打开登录浮层");
  //   log(this.getAttribute("tag"));
  // };
  // var log = function (tag) {
  //   console.log("上报标签为" + tag);
  // };
  // document.getElementById("button").onclick = showLogin;

  // 以上登录按钮又负责打开浮层又负责数据上报,这是两个层面的功能,却被耦合在一个函数里
  // 试试 AOP 实现分离的威力
  Function.prototype.after = function (afterFn) {
    var _self = this;
    return function () {
      var ret = _self.apply(this, arguments);
      afterFn.apply(this, arguments);
      return ret;
    };
  };

  var showLogin = function () {
    console.log("打开登录浮层");
  };

  var log = function () {
    console.log("上报标签为" + this.getAttribute("tag"));
  };

  showLogin = showLogin.after(log); // 打开浮层后上报数据
  document.getElementById("button").onclick = showLogin;
</script>

3. AOP动态改变函数的参数

Function.prototype.before = function (beforeFn) {
  var _self = this;
  return function () {
    beforeFn.apply(_self, arguments);
    return _self.apply(this, arguments);
  };
};

var func = function (param) {
  console.log(param);
};

func = func.before((param) => (param.b = "b"));
func({ a: "a" }); // 输出{b:'b',a:'a'}

// 观察上面的例子 现在有一个用于发请求的ajax函数
var ajax = function (type, url, param) {
  console.log(param);
  // 请求代码略
};
ajax("get", "http://xxx", { name: "张三" });

// 如果此时服务器遭到CSRF攻击,需要加入token参数
var getToken = () => "Token";

var ajax1 = function (type, url, param) {
  param = param || {};
  param.token = getToken();
};

// 这样看起来是没什么问题 ,但是将来移植到另一个项目 token是多余的
// 解决
ajax = ajax.before((type, url, param) => (param.token = getToken()));
ajax("get", "http://xxx", { name: "张三" }); // 打印{name:'张三',token:'Token'}

3. 插件式表单验证

// 插件式表单验证
// 以下username,password submitBtn均为document.getElementById获取的表单
Function.prototype.before = function (beforeFn) {
  var _self = this;
  return function () {
    if (beforeFn.apply(_self, arguments) === false) {
      return;
    }
    return _self.apply(this, arguments);
  };
};

var validate = function () {
  if (username.value === "") {
    alert("用户名不能为空");
    return false;
  }
  if (password.value === "") {
    alert("密码不能为空");
    return false;
  }
};

var formSubmit = function () {
  var param = {
    username: username.value,
    password: password.value,
  };
  ajax("http://xxx", param);
};

formSubmit = formSubmit.before(validate); // 插件式安装 即插即用

submitBtn.onClick = function () {
  formSubmit();
};

十三、状态模式

状态模式是一种非同寻常的优秀模式,它也许是解决某些需求场景最好办法,虽然不是一个看起来一目了然的模式,但是学会了它的精髓,将来你一定会感谢它带给你无与伦比的好处。

允许一个对象在其内部状态改变时,改变它的行为,对象看起来似乎修改了它的类。

将状态分为独立的类,并将请求委托给当前的状态对象,当对象内部状态改变时,会带来不同的行为变化。

电灯的例子足以说明这一点。看起来是不同的类中实例化而来的,实际上是使用了委托的效果

1.电灯案例

// 状态机
var Light = function () {
  this.state = "off";
  this.button = null;
};

Light.prototype.init = function () {
  var button = document.createElement("button"),
    self = this;
  button.innerHTML = "开关";
  this.button = document.body.appendChild(button);
  this.button.onclick = function () {
    self.buttonWasPressed();
  };
};

Light.prototype.buttonWasPressed = function () {
  if (this.state == "off") {
    console.log("开灯");
    this.state = "on";
  } else if (this.state == "on") {
    console.log("关灯");
    this.state = "off";
  }
};

var light = new Light();
light.init();
// 单从以上代码看是没有一点bug的,无懈可击 逻辑慎密。但是
// 如果是另一种灯,就很明显的不符合开放封闭原则了
Light.prototype.buttonWasPressed = function () {
  if (this.state == "off") {
    console.log("弱光");
    this.state = "weakLight";
  } else if (this.state == "weakLight") {
    console.log("强光");
    this.state = "strongLight";
  } else if ((this.state = "strongLight")) {
    console.log("关灯");
    this.state = "off";
  }

利用状态模式改造

// 关闭的状态类
var OffLightState = function (light) {
  this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
  console.log("弱光"); // 对应的行为
  this.light.setState(this.light.weakLighatState); // 切换到该状态
};

// 弱光的状态类
var WeakLightState = function (light) {
  this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
  console.log("强光");
  this.light.setState(this.light.strongLighatState); // 切换到该状态
};

// 强光的状态类
var StrongLightState = function (light) {
  this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
  console.log("关灯"); 
  this.light.setState(this.light.offLightState);
  // console.log("超强光"); 
  // this.light.setState(this.light.superLightState);
};

// var SuperLightState = function (light) {
//   this.light = light;
// };

// SuperLightState.prototype.buttonWasPressed = function() {
//   console.log("关灯");
//   this.light.setState(this.light.offLightStat);
// }

// 为了方便看到电灯类有多少种状态,不在用字符串表示,而逝用更立体的构造函数
var Light = function () {
  // 创建每一个状态类的实例对象,便于后续委托
  this.offLightState = new OffLightState(this);
  this.weakLighatState = new WeakLightState(this);
  this.strongLighatState = new StrongLightState(this);
  this.superLightState = new SuperLightState(this);
  this.button = null;
};

Light.prototype.init = function () {
  var button = document.createElement("button"),
    self = this;
  this.button = document.body.appendChild(button);
  this.button.innerHTML = "开关";
  this.currState = this.offLightState;

  // 定义用户的请求动作
  this.button.onclick = function () {
    self.currState.buttonWasPressed(); // 委托给当前持有的状态执行
  };
};

// 最后准备一个切换状态的方法
Light.prototype.setState = function (newState) {
  this.currState = newState;
};

// 现在可以进行一些测试
var light = new Light();
light.init();

// 现在可以很明显的看到每一种状态与它对应的行为关系局部化,无需编写大量if else控制状态之间的转换
// 假设多了一个超强光的状态 见文中注释

我们注意到以上委托对象的方法是buttonWasPressed,无论加多少状态都不能忘了这个,可以进行约束

// 利用模板方法模式约束
var State = function () {};
State.prototype.buttonWasPressed = function () {
  throw new Error("父类的buttonWasPressed必须被重写");
};

var SuperLightState = function (light) {
  this.light = light;
};

SuperLightState.prototype = new State(); // 继承抽象父类
// 重写buttonWasPressed方法
SuperLightState.prototype.buttonWasPressed = function () {
  console.log("关灯");
  this.light.setState(this.light.offLightStat);
};

2.文件上传案例

先看一下反例的实现

<script>
  window.external.upload = function (state) {
    console.log(state);
  };

var plugin = (function () {
  var plugin = document.createElement("embed");
  plugin.style.display = "none";
  plugin.type = "application/txftn-webkit";
  plugin.sign = function () {
    console.log("开始扫描");
  };
  plugin.pause = function () {
    console.log("暂停上传");
  };
  plugin.uploading = function () {
    console.log("开始上传");
  };
  plugin.del = function () {
    console.log("删除文件");
  };
  plugin.done = function () {
    console.log("上传完成");
  };
  document.body.appendChild(plugin);
  return plugin;
})();

var Upload = function (fileName) {
  this.plugin = plugin;
  this.fileName = fileName;
  this.button1 = null;
  this.button2 = null;
  this.state = "sign"; // 设置初始状态为waiting
};

Upload.prototype.init = function () {
  var that = this;
  this.dom = document.createElement("div");
  this.dom.innerHTML = `<span>文件名称:${this.fileName}</span>\n<button data-action="button1">扫描中</button>\n<button data-action="button2">删除</button>`;
  document.body.appendChild(this.dom);
  this.button1 = document.querySelector("[data-action='button1']");
  this.button2 = document.querySelector("[data-action='button2']");
  this.bindEvent();
};

Upload.prototype.bindEvent = function () {
  var self = this;
  this.button1.onclick = function () {
    if (self.state === "sign") {
      console.log("扫描中,点击无效");
    } else if (self.state === "uploading") {
      self.changeState("pause");
    } else if (self.state === "pause") {
      self.changeState("uploading");
    } else if (self.state === "done") {
      console.log("已完成上传,点击无效");
    } else if (self.state === "error") {
      console.log("上传失败,点击无效");
    }
  };

  this.button2.onclick = function () {
    if (
      self.state === "done" ||
      self.state === "error" ||
      self.state === "pause"
    ) {
      self.changeState("del");
    } else if (self.state === "sign") {
      console.log("正在扫描,不能删除");
    } else if (self.state === "uploading") {
      console.log("正在上传,不能删除");
    }
  };
};

Upload.prototype.changeState = function (state) {
  switch (state) {
    case "sign": {
      this.plugin.sign();
      this.button1.innerHTML = "扫描中,任何操作无效";
      break;
    }
    case "uploading": {
      this.plugin.uploading();
      this.button1.innerHTML = "正在上传,点击暂停";
      break;
    }
    case "pause": {
      this.plugin.pause();
      this.button1.innerHTML = "已暂停,点击上传";
      break;
    }
    case "done": {
      this.plugin.done();
      this.button1.innerHTML = "上传完成";
      break;
    }
    case "error": {
      this.button1.innerHTML = "上传失败";
      break;
    }
    case "del": {
      this.plugin.del();
      this.dom.parentNode.removeChild(this.dom);
      console.log("删除成功");
      break;
    }
  }
  this.state = state;
};

// 测试
var uploadObj = new Upload("啊小勇");
uploadObj.init();
window.external.upload = function (state) {
  uploadObj.changeState(state);
};
window.external.upload("sign");

setTimeout(() => {
  window.external.upload("uploading");
}, 1000);

setTimeout(() => {
  window.external.upload("done");
}, 5000);
</script>

使用状态模式改造文件上传

<script>
  // 第一步 模拟上传插件(没变化)
  window.external.upload = function (state) {
    console.log(state); // 可能为 sign uploading done error
  };
var plugin = (function () {
  var plugin = document.createElement("embed");
  plugin.style.display = "none";
  plugin.type = "application/txftn-webkit";
  plugin.sign = function () {
    console.log("开始扫描");
  };
  plugin.pause = function () {
    console.log("暂停上传");
  };
  plugin.uploading = function () {
    console.log("开始上传");
  };
  plugin.del = function () {
    console.log("删除文件");
  };
  plugin.done = function () {
    console.log("上传完成");
  };
  document.body.appendChild(plugin);
  return plugin;
})();

// 第二步改造Upload构造函数,为每种状态子类创建实例对象
var Upload = function (fileName) {
  this.plugin = plugin;
  this.fileName = fileName;
  this.button1 = null;
  this.button2 = null;
  this.signState = new SignState(this);
  this.uploadingState = new UploadingState(this);
  this.pauseState = new PauseState(this);
  this.doneState = new DoneState(this);
  this.errorState = new ErrorState(this);
  this.currState = this.signState; // 设置初始状态为waiting
};

// 第三步不用改
Upload.prototype.init = function () {
  var that = this;
  this.dom = document.createElement("div");
  this.dom.innerHTML = `<span>文件名称:${this.fileName}</span>\n<button data-action="button1">扫描中</button>\n<button data-action="button2">删除</button>`;
  document.body.appendChild(this.dom);
  this.button1 = document.querySelector("[data-action='button1']");
  this.button2 = document.querySelector("[data-action='button2']");
  this.bindEvent();
};

// 第四步点击按钮后context不做任何事 而是委托当前状态类执行
Upload.prototype.bindEvent = function () {
  var self = this;
  this.button1.onclick = function () {
    self.currState.clickHandler1();
  };
  this.button2.onclick = function () {
    self.currState.clickHandler2();
  };
};

Upload.prototype.sign = function () {
  this.plugin.sign();
  this.currState = this.signState;
};

Upload.prototype.uploading = function () {
  this.button1.innerHTML = "正在上传,点击暂停";
  this.plugin.uploading();
  this.currState = this.uploadingState;
};

Upload.prototype.pause = function () {
  this.button1.innerHTML = "已暂停,点击上传";
  this.plugin.pause();
  this.currState = this.pauseState;
};

Upload.prototype.done = function () {
  this.button1.innerHTML = "上传完成";
  this.plugin.done();
  this.currState = this.doneState;
};

Upload.prototype.error = function () {
  this.button1.innerHTML = "上传失败";
  this.currState = this.errorState;
};

Upload.prototype.del = function () {
  this.plugin.del();
  this.dom.parentNode.removeChild(this.dom);
};

var StateFactory = (function () {
  var State = function () {};
  State.prototype.clickHandler1 = function () {
    throw new Error("子类必须重新定义父类的clickHandler1方法");
  };
  State.prototype.clickHandler2 = function () {
    throw new Error("子类必须重新定义父类的clickHandler2方法");
  };

  return function (param) {
    var F = function (uploadObj) {
      this.uploadObj = uploadObj;
    };
    F.prototype = new State();
    for (var i in param) {
      F.prototype[i] = param[i];
    }
    return F;
  };
})();

var SignState = StateFactory({
  clickHandler1: function () {
    console.log("扫描中,点击无效");
  },
  clickHandler2: function () {
    console.log("上传中,不能删除");
  },
});

var UploadingState = StateFactory({
  clickHandler1: function () {
    this.uploadObj.pause();
  },
  clickHandler2: function () {
    console.log("上传中,不能删除");
  },
});

var PauseState = StateFactory({
  clickHandler1: function () {
    this.uploadObj.uploading();
  },
  clickHandler2: function () {
    this.uploadObj.del();
  },
});

var DoneState = StateFactory({
  clickHandler1: function () {
    console.log("已完成上传,点击无效");
  },
  clickHandler2: function () {
    this.uploadObj.del();
  },
});

var ErrorState = StateFactory({
  clickHandler1: function () {
    console.log("上传失败,点击无效");
  },
  clickHandler2: function () {
    this.uploadObj.del();
  },
});

// 测试
var uploadObj = new Upload("啊小勇");
uploadObj.init();
window.external.upload = function (state) {
  uploadObj[state]();
};
window.external.upload("sign");

setTimeout(() => {
  window.external.upload("uploading"); // 1s后开始上传
}, 1000);

setTimeout(() => {
  window.external.upload("done"); // 5s后结束上传
}, 5000);
</script>

3.javascript版本的开关灯案例

var Light = function () {
  this.currState = FSM.off; // 设置当前状态
  this.button = null;
};

Light.prototype.init = function () {
  var button = document.createElement("button"),
    self = this;
  button.innerHTML = "已关灯";
  this.button = document.body.appendChild(button);
  this.button.onclick = function () {
    self.currState.buttonWasPressed.call(self); // 把请求委托给FSM状态机
  };
};

var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log("关灯");
      this.button.innerHTML = "开灯";
      this.currState = FSM.on;
    },
  },
  on: {
    buttonWasPressed: function () {
      console.log("开灯");
      this.button.innerHTML = "关灯";
      this.currState = FSM.off;
    },
  },
};

var light = new Light();
light.init();

4.闭包形式的开关灯

// 闭包
var delegate = function (client, delegation) {
  return {
    // 将客户操作委托delegation对象
    buttonWasPressed: function () {
      return delegation.buttonWasPressed.apply(client, arguments);
    },
  };
};

var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log("关灯");
      this.button.innerHTML = "开灯";
      this.currState = FSM.on;
    },
  },
  on: {
    buttonWasPressed: function () {
      console.log("开灯");
      this.button.innerHTML = "关灯";
      this.currState = FSM.off;
    },
  },
};

var Light = function () {
  this.offState = delegate(this, FSM.off);
  this.onState = delegate(this, FSM.on);
  this.currState = this.offState;
  this.button = null;
};

Light.prototype.init = function () {
  var button = document.createElement("button"),
    self = this;
  button.innerHTML = "已关灯";
  this.button = document.body.appendChild(button);
  this.button.onclick = function () {
    self.currState.buttonWasPressed();
  };
};

var light = new Light();
light.init();

十四、适配器模式

作用是解决两个软件实体间接口不兼容的问题,这是一个亡羊补牢的模式,我们不会在程序设计之初使用它。通过下面的实例可以深刻的理解它。

1.渲染地图的例子

var googleMap = {
  show: function () {
    console.log("渲染谷歌地图");
  },
};
var baiduMap = {
  show: function () {
    console.log("渲染百度地图");
  },
};
var render = function (map) {
  if (map.show instanceof Function) {
    map.show();
  }
};

render(googleMap);
render(baiduMap);

// 如果有一天百度地图的show方法名字变成display
var baiduMap = {
  display: function () {
    console.log("渲染百度地图");
  },
};

// 利用适配器模式
var baiduMapAdapter = {
  show: function () {
    return baiduMap.display();
  },
};
// 解决
render(baiduMapAdapter);

2,渲染广东地图的例子

var getGuangdongCity = function () {
  var guangdongCity = [
    { name: "shenzhen", id: 11 },
    { name: "guangzhou", id: 12 },
  ];
  return guangdongCity;
};

var render = function (fn) {
  console.log("渲染广东地图");
  document.write(JSON.stringify(fn));
};
render(getGuangdongCity);

// 将来有一天发现这些数据不可靠,替换一种更全面的数据。遗憾的是数据结构不一样
var guangdongCity = {
  shenzhen: 11,
  guangzhou: 12,
  zhuhai: 13,
};
// 这时可以使用适配器模式
var addressAdapter = function (oldAddressFn) {
  var address = {},
    oldAddress = oldAddressFn();

  for (var i = 0, c; (c = oldAddress[i++]); ) {
    address[c.name] = c.id;
  }

  return function () {
    return address;
  };
};
// 接下来 把代码中调用过getGuangdongCity的地方,用经过addressAdapter转换后的新函数代替
render(addressAdapter(getGuangdongCity));

至此js基本上常用的设计模式均已介绍完毕

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值