javascript设计模式,还可以这样玩!!!

300 阅读23分钟

前言

纯属是为了敲一遍代码而写,觉得有帮助的大佬们可以给个赞喔。后面,有提供github链接以及此内容的书籍。

单例模式

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

说明: 要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。当然我们要把创建对象和管理单例的职责分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。

使用场景: 单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏 览器中的 window 对象等。在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我 们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

核心代码:

    /*
     * 单例模式
     * @params {Function} 创建实例的方法
     * @return 唯一的实例
     */
    var getSingle = function (fn) {
      var result
      return function () {
        return result || (result = fn.apply(this, arguments))
      }
    }	

例子:

    /*
     * 单例模式
     * @params {Function} 创建实例的方法
     * @return 唯一的实例
     */
    var getSingle = function (fn) {
      var result
      return function () {
        return result || (result = fn.apply(this, arguments))
      }
    }

    var createObj = function () {
      return {
        name: '我是单例模式,只创建一次'
      }
    }

    var createSingleObj = getSingle(createObj)

    console.log(createSingleObj()) // { name: '我是单例模式,只创建一次' }
    console.log(createSingleObj() === createSingleObj()) // true

策略模式

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

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

使用场景: 在程序设计中,我们也常常遇到类似的情况,要实现某一个功能有多种方案可以选择。比如一个压缩文件的程序,既可以选择 zip 算法,也可以选择 gzip 算法;不同绩效计算奖金;表单校验等等。

核心代码&&例子:

    /*
     * 策略模式
     */
    var strategies = { // 策略对象
      'A': function (params) {
        return params + 'A'
      },
      'B': function (params) {
        return params + 'B'
      },
      'C': function (params) {
        return params + 'C'
      }
    }

    var useStrategy = function (strategy, params) {
      return strategies[strategy](params)
    }

    console.log(useStrategy('A', '你好')) // 你好A
    console.log(useStrategy('C', '你好')) // 你好C

代理模式

定义: 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

说明: 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

使用场景: 虚拟代理图片的预加载;缓存代理用于ajax异步请求数据;保护代理拦截不符合要求的请求等等。

虚拟代理

图片预加载技术例子: 图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性, 由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种 场景就很适合使用虚拟代理。

    /*
     * 虚拟代理
     * 图片预加载的实现
     */
    var image = (function () { // 展示图片
      var imgNode = document.createElement('img')
      document.body.appendChild(imgNode)
      return function (src) {
        imgNode.src = src
      }
    })()

    var proxyImage = (function () { // 代理请求图片
      var img = new Image()
      img.onload = function () { // 图片请求完毕,直接展示
        image(this.src)
      }
      return function (src) {
        image('<本地图片地址>')
        img.src = src
      }
    })()

    proxyImage('<服务器的图片地址>')

缓存代理

说明: 缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参 数跟之前一致,则可以直接返回前面存储的运算结果。下面的例子是同步的,如果是异步的话,可以通过回调函数把结果存放到代理对象缓存中。比如:ajax异步请求数据。

核心代码&&例子:

    /*
     * 缓存代理
     * 乘积的缓存实现
     */
    var mult = function () { // 乘积函数
      var result = 1
      for(var i = 0, len = arguments.length; i < len; i++){
        result *= arguments[i]
      }
      return result
    }

    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)
      }
    })()

    console.log(proxyMult(1, 2, 3, 4, 5)) // 120 //计算出来的结果
    console.log(proxyMult(1, 2, 3, 4, 5)) // 120 //使用缓存获取的结果

迭代器模式

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

说明: 迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前 的绝大部分语言都内置了迭代器。我们平时用的Array.property.forEach()for...in...for...of...这些都是迭代器。

使用场景: 循环访问聚合对象中的各个元素

例子:

    /*
     * 迭代器模式
     */
    var each = function (array, callback) { // 迭代器
      for (var i = 0, len = array.length; i < len; i++) {
        callback.call(array[i], i, array[i])
      }
    }

    var fn = function (index, item) { // 数组的下标 index 数组的项 item
      console.log(index, item)
    }
    each(['value1', 'value2', 'value3'], fn) // 0 "value1" 1 "value2" 2 "value3"

例子: JavaScript 中的 Array.prototype[@@iterator]() 会返回一个迭代器对象,类似下面的例子,@@iterator 这个是 javascript 内置实现的

    /*
     * 迭代器模式
     * ES5语法模拟JavaScript中的迭代器
     */
    var createIteratorObj = function (items) { // 生成迭代器对象
      var i = 0
      return {
        next: function () {
          var done = (i >= items.length)
          var value = !done ? items[i++] : undefined

          return { // next() 方法返回结果对象 value是值,done为true的时候代表迭代结束
            value: value,
            done: done
          }
        }
      }
    }
    var iterator = function (iteratorObj) { // 迭代器
      var item = null
      do {
        item = iteratorObj.next() // 返回 {value: xxx, done: xxx}
        console.log(item.value)
      }while (!item.done)
    }

    var iteratorObj = createIteratorObj(['value1', 'value2', 'value3'])
    iterator(iteratorObj) // value1 value2 value3 undefined

发布-订阅模式

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

说明: 发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM, 都少不了发布—订阅模式的参与,而且 JavaScript 本身也是一门基于事件驱动的语言。 当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个 bug 不是件轻松的事情。 在 JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。

例子:

    /*
     * 发布-订阅者模式
     * 需要手动安装的对象
     */
    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) // 取出第一个事件名参数,剩余的是数据参数
        var fns = this.clientList[key]
        if (!fns || fns.length === 0) { // 没有绑定订阅信息
          return false
        }
        for ( var i = 0, fn; fn = fns[i]; i++ ) { // 遍历对应订阅事件的回调函数
          fn.apply(this, arguments)
        }
      },
      remove: function (key, fn) { // 取消订阅事件
        var fns = this.clientList[key]
        if (!fns) { // key对应的消息没有被订阅,则直接返回
          return false
        }
        if (!fn) { // 没有传入具体的回调函数,则取消对应key的所有订阅
         return fns && (fns.length = 0)
        }
        // 只需要取消相应的订阅回调函数
        for (var len = fns.length - 1; len >= 0; len--) {
          var _fn = fns[len]
          if (_fn === fn) {
            fns.splice(len, 1) // 删除订阅的回调函数
          }
        }
      }
    }
    
    var installEvent = function (obj) { // 给对象安装发布-订阅功能
      for (var i in event) { // 遍历发布-订阅者事件对象
        obj[i] = event[i]
      }
    }

    // 使用
    var obj = {}
    installEvent(obj) // 为obj安装发布-订阅功能
    var myFn1 = function (params) {
      console.log(params)
    }
    var myFn2 = function (params) {
      console.log(params)
    }
    obj.listen('myClick', myFn1) // 添加订阅
    obj.listen('myClick', myFn2)
    obj.trigger('myClick', 'success') // 发布订阅 输出:success success
    obj.remove('myClick', myFn1) // 取消订阅
    obj.trigger('myClick', 'afterRemove') // 发布订阅 输出:afterRemove

核心代码 && 例子:

    /*
     * 发布-订阅者模式
     * 全局的订阅-发布对象
     */
    var event = (function () {
      var clientList = [] // 订阅信息列表

      var listen = function (key, fn) { // 订阅事件
        debugger
        if (!clientList[key]) {
          clientList[key] = []
        }
        clientList[key].push(fn)
      }

     var trigger = function () { // 发布事件
        var key = Array.prototype.shift.call(arguments) // 取出第一个事件名参数,剩余的是数据参数
        var fns = clientList[key]
        if (!fns || fns.length === 0) { // 没有绑定订阅信息
          return false
        }
        for ( var i = 0, fn; fn = fns[i]; i++ ) { // 遍历对应订阅事件的回调函数
          fn.apply(this, arguments)
        }
      }

      var remove = function (key, fn) { // 取消订阅事件
        var fns = clientList[key]
        if (!fns) { // key对应的消息没有被订阅,则直接返回
          return false
        }
        if (!fn) { // 没有传入具体的回调函数,则取消对应key的所有订阅
         return fns && (fns.length = 0)
        }
        // 只需要取消相应的订阅回调函数
        for (var len = fns.length - 1; len >= 0; len--) {
          var _fn = fns[len]
          if (_fn === fn) {
            fns.splice(len, 1) // 删除订阅的回调函数
          }
        }
      }

      return { // 暴露出去的方法
        listen,
        trigger,
        remove
      }
    })()
    
    // 使用
    var myFn1 = function (params) {
      console.log(params)
    }
    var myFn2 = function (params) {
      console.log(params)
    }
    event.listen('myClick', myFn1) // 添加订阅
    event.listen('myClick', myFn2)
    event.trigger('myClick', 'success') // 发布订阅 输出:success success
    event.remove('myClick', myFn1) // 取消订阅
    event.trigger('myClick', 'afterRemove') // 发布订阅 输出:afterRemove

提醒: 必须先订阅再发布吗?上面的代码如果先发布再订阅的话,发布的消息就会丢失,无法成功订阅到。那如果我们要实现可以先发布再订阅的功能怎么办?为了满足这个需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,所以刚才的操作我们只能进行一次。

命令模式

定义: 命令模式中的命令(command)指的是一个执行某些特定事情的指令。

说明: 有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

核心代码 && 例子:

宏命令例子:宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令,他是命令模式与组合模式的联用产物。想象一下,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,然后开空调,最后打开电视。

    /*
     * 命令模式
     * 宏命令例子
     */
    var closeDoorCommand = {
      execute: function () {
        console.log('关门')
      }
    }
    var openFanCommand = {
      execute: function () {
        console.log('开风扇')
      }
    }
    var openTelevisionCommand = {
      execute: function () {
        console.log('开电视机')
      }
    }

    var MacroCommand = function () { // 宏命令
      return {
        commandLists: [], // 命令列表
        add: function (command) {
          this.commandLists.push(command)
        },
        execute: function () {
          var command // 命令 function
          for (var i = 0, len = this.commandLists.length; i < len; i++) {
            command = this.commandLists[i]
            command.execute()
          }
        }
      }
    }

    // 使用
    var macroCommand = MacroCommand()
    macroCommand.add(closeDoorCommand)
    macroCommand.add(openFanCommand)
    macroCommand.add(openTelevisionCommand)
    macroCommand.execute() // 输出:关门 开风扇 开电视机

组合模式

定义: 组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更 小的“孙对象”构成的。

说明: 组合模式可以让我们使用树形方式创 建对象的结构。我们可以把相同的操作应用在组合对象和单个对象上。在大多数情况下,我们都可以忽略掉组合对象和单个对象之间的差别,从而用一致的方式来处理它们。 然而,组合模式并不是完美的,它可能会产生一个这样的系统:系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来,这会使代码难以理解。此外, 如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。

使用场景: 扫描文件夹、遥控器命令

核心代码&&例子:

基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合, 这样不断递归下去,这棵树的结构可以支持任意多的复杂度。在树最终被构造完成之后,让整颗树最终运转起来的步骤非常简单,只需要调用最上层对象的 execute 方法。每当对最上层的对象 进行一次请求时,实际上是在对整个树进行深度优先的搜索,而创建组合对象的程序员并不关心这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情。

    /*
     * 命令模式
     * 展示宏命令中组合模式的强大例子
     */
    var closeDoorCommand = {
      execute: function () {
        console.log('关门')
      }
    }
    var openFanCommand = {
      execute: function () {
        console.log('开风扇')
      }
    }
    var openTelevisionCommand = {
      execute: function () {
        console.log('开电视机')
      }
    }

    var openSoundCommand = {
      execute: function () {
        console.log('开音响')
      }
    }

    var MacroCommand = function () { // 宏命令
      return {
        commandLists: [], // 命令列表
        add: function (command) {
          this.commandLists.push(command)
        },
        execute: function () {
          var command // 命令 function
          for (var i = 0, len = this.commandLists.length; i < len; i++) {
            command = this.commandLists[i]
            command.execute()
          }
        }
      }
    }

    // 使用
    var macroCommand1 = MacroCommand()
    macroCommand1.add(openTelevisionCommand)
    macroCommand1.add(openSoundCommand)

    var macroCommand2 = MacroCommand()
    macroCommand2.add(closeDoorCommand)
    macroCommand2.add(openFanCommand)
    macroCommand2.add(macroCommand1)
    macroCommand1.execute() // 输出:开电视机 开音响
    macroCommand2.execute() // 输出:关门 开风扇 开电视机 开音响

模板方法模式

说明: 模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

核心代码&&例子:

    /*
     * 模板方法模式
     * 咖啡和茶的经典例子
     */
     var Beverage = function () {}
     Beverage.prototype.boilWater = function () { // 煮水
        console.log('把水煮沸')
     }
     Beverage.prototype.brew = function () { // 冲泡
        throw new Error('子类必须重写brew方法') // 防止没有重写此方法
     }
     Beverage.prototype.pourInCup = function () { // 倒进杯子
        throw new Error('子类必须重写pourInCup方法')
     }
     Beverage.prototype.addCondiments = function () { // 添加调味料
        throw new Error('子类必须重写addCondiments方法')
     }
     Beverage.prototype.customerWantsCondiments = function () { // 自定义是否需要调料 hook
        return true // 默认需要调料
     }
     Beverage.prototype.init = function () {
       this.boilWater()
       this.brew()
       this.pourInCup()
       if (this.customerWantsCondiments()) { // 如果钩子函数返回true,则需要调料
          this.addCondiments()
       }
     }

     var CoffeeWithHook = function () {}
     CoffeeWithHook.prototype = new Beverage() // 继承饮料的类
     CoffeeWithHook.prototype.brew = function () {
       console.log('用沸水冲泡咖啡')
     }
     CoffeeWithHook.prototype.pourInCup = function () {
       console.log('把咖啡倒进杯子')
     }
     CoffeeWithHook.prototype.addCondiments = function () {
       console.log('加糖和牛奶')
     }
     CoffeeWithHook.prototype.customerWantsCondiments = function () {
       return window.confirm('请问需要调料吗?')
     }

     var coffeeWithHook = new CoffeeWithHook() // 创建咖啡的实例
     //确认需要调料 输出:把水煮沸 用沸水冲泡咖啡 把咖啡倒进杯子 加糖和牛奶
     //不需要调料 输出:把水煮沸 用沸水冲泡咖啡 把咖啡倒进杯子
     coffeeWithHook.init() // 初始化实例

享元模式

说明: 享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非 常有意义的事情。享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在 大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。

使用场景: 文件上传、对象池

核心代码&&例子:

    /*
     * 享元模式
     * 文件上传例子
     */

    /**
     * Upload类
     * 内外部状态分离
     * @params {String} uploadType 上传文件的类型
     */
    var Upload = function (uploadType) { // uploadType 内部状态,不变
      this.uploadType = uploadType
    }

    Upload.prototype.delFile = function (id) { // id 外部状态,变化
      uploadManage.setExternalState(id, this)
      if (this.fileSize < 3000) { // 当文件小于3000KB时,直接删除
        return this.dom.parentNode.removeChild(this.dom)
      }
      if (window.confirm('确定要删除文件吗?' + this.fileName)){ // 用户确认后删除
        return this.dom.parentNode.removeChild(this.dom)
      }
    }

    /**
     * upload 创建工厂
     */
    var UploadFactory = (function () { // upload 对象实例化工厂
      var createFlyWeightObjs = {}

      return {
        create: function (uploadType) { // 创建一个实例
          if(createFlyWeightObjs[uploadType]) { // 如果享元对象中有此类型数据,则直接返回,否则创建一个新的实例,并保存到享元对象上
            return createFlyWeightObjs[uploadType]
          }
          return createFlyWeightObjs[uploadType] = new Upload(uploadType)
        }
      }
    })()

    /**
     * 外部状态管理器
     * @pramas {Number} id 标识每个文件的唯一 id
     * @params {String} uploadType 上传文件的类型
     * @params {String} fileName 上传文件名称
     * @params {Number} fileSize 上传文件大小
     * @params {Object} flyWeightObj 享元对象
     */
    var uploadManage = (function () {
      var uploadDatabase = {}

      return {
        add: function (id, uploadType, fileName, fileSize) { // 添加上传的文件
          var flyWeightObj = UploadFactory.create(uploadType) // 创建一个upload 实例

          var dom = document.createElement('div') // 创建文件列表元素
          dom.innerHTML = '<span>文件名称:' + fileName + ', 文件大小:' + fileSize + '</span>' + '<button class="delFile">删除</button>'
          dom.querySelector('.delFile').onclick = function () {
            flyWeightObj.delFile(id)
          }
          document.body.appendChild(dom)

          uploadDatabase[id] = { // 保存 upload 对象的外部状态
            fileName: fileName,
            fileSize: fileSize,
            dom: 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 (uploadType, files) {
      for(var i = 0, file; files[i]; i++) {
        file = files[i]
        var uploadObj = uploadManage.add( ++id, uploadType, file.fileName, file.fileSize)
      }
    }

    // 测试例子
    startUpload('plugin', [
      {
        fileName: '1.txt',
        fileSize: 1000
      },
      {
        fileName: '2.txt',
        fileSize: 2000
      },
      {
        fileName: '3.txt',
        fileSize: 3000
      }
    ])

    startUpload('flash', [
      {
        fileName: '4.txt',
        fileSize: 4000
      },
      {
        fileName: '5.txt',
        fileSize: 5000
      },
      {
        fileName: '6.txt',
        fileSize: 6000
      }
    ])

职责链模式

定义: 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间 的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

说明: 职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点。

使用场景: 优惠券

同步职责链核心代码&&例子:

优惠卷的三种优惠情况:

  • 已支付500定金,则有100优惠券
  • 已支付200定金,则有50优惠卷
  • 没有支付任何定金,则没有优惠卷
    /*
     * 职责链模式
     * 同步职责链
     * 优惠券例子
     */
     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
     }
  
     /**
      * @pramas {Number} orderType 订单类型
      * @params {Boolean} isPay 是否已支付定金
      * @params {Number} stock 库存
      */
     var order500 = function (orderType, isPay, stock) { // 500定金的订单
        if(orderType === 1 && isPay === true) {
          console.log('已支付500元定金,得到100优惠券')
        }else {
          return 'nextSuccessor' // 下一个节点
        }
     }
     var order200 = function (orderType, isPay, stock) { // 200定金的订单
        if(orderType === 2 && isPay === true) {
          console.log('已支付200元定金,得到50优惠券')
        }else {
          return 'nextSuccessor' // 下一个节点
        }
     }
     var orderNormal = function (orderType, isPay, stock) { // 无定金的订单
        if(stock > 0) {
          console.log('无优惠券购买')
        }else {
          console.log('库存不足')
        }
     }

     var chainOrder500 = new Chain(order500) // 创建职责链节点实例
     var chainOrder200 = new Chain(order200)
     var chainOrderNormal = new Chain(orderNormal)

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

     // 测试
     chainOrder500.passRequest( 1, true, 500 ); // 输出:已支付500元定金,得到100优惠券
     chainOrder500.passRequest( 2, true, 500 ); // 输出:已支付200元定金,得到50优惠券
     chainOrder500.passRequest( 3, true, 500 ); // 输出:无优惠券购买
     chainOrder500.passRequest( 1, false, 0 ); // 输出:库存不足

异步职责链核心代码&&例子:

如果职责链的节点中存在异步请求,直接返回"nextSuccessor"是不行的。解决方案:可以给Chain类增加一个原型方法Chain.prototype.next,手动的传递请求给职责链的下一个请求。

    /*
     * 职责链模式
     * 异步职责链
     */
     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
     }
     Chain.prototype.next = function () { // 手动操作将请求传递给下一个节点
      return this.successor && this.successor.passRequest.apply(this.successor, arguments)
     }
     
     var chainFn1 = new Chain(function() { // 创建职责链类的实例
       console.log(1)
       return 'nextSuccessor'
     })
     var chainFn2 = new Chain(function() {
       console.log(2)
       setTimeout(() => {
         this.next() // 手动传递请求给职责链的下个节点
       }, 100)
     })
     var chainFn3 = new Chain(function() {
       console.log(3)
     })

     chainFn1.setNextSuccessor(chainFn2).setNextSuccessor(chainFn3) // 设置职责链的顺序

     chainFn1.passRequest() // 输出:1 2 3

中介者模式

说明: 中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。不过,中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因 为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。一般来说, 如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。

使用场景: 多人竞技游戏

核心代码&&例子:

    /*
     * 中介者模式
     * 多人竞技游戏例子
     */

      /**
       * 玩家类
       * @params {String} name 玩家名字
       * @params {String} teamColor 玩家队伍颜色
       */
     function Player (name, teamColor) {
      this.name = name
      this.teamColor = teamColor
      this.state = 'alive' //玩家生存状态
     }
     Player.prototype.win = function () { // 玩家胜利
       console.log(this.name + 'won')
     }
     Player.prototype.lose = function () { // 玩家失败
       console.log(this.name + 'lost')
     }
     Player.prototype.die = function () { // 玩家死亡
       this.state = 'dead'
       playerDirector.reciveMessage('playerDead', this) // 给中介者发送消息,玩家死亡
     }
     Player.prototype.remove = function () { //移除玩家
      playerDirector.reciveMessage('removePlayer', this) // 给中介者发送消息,移除一个玩家      
     }
     Player.prototype.changeTeam = function (color) { //玩家切换队伍
      playerDirector.reciveMessage('changeTeam', this, color) // 给中介者发送消息,玩家换队
     }

     /**
      * 创建玩家类实例的工厂函数
      * @params {String} name 玩家名字
      * @params {String} teamColor 玩家队伍颜色
      */
     var playerFactory = function (name, teamColor) {
      var newPlayer = new Player(name, teamColor)
      playerDirector.reciveMessage('addPlayer', newPlayer) // 给中介者发送消息,新增玩家
      return newPlayer
     }

     /**
      * 中介者 playerDirector (这两种方式的实现没什么本质上的区别,使用方案二实现)
      * 方案一、利用发布—订阅模式。将 playerDirector 实现为订阅者,各 player 作为发布者,一旦 player
      * 的状态发生改变,便推送消息给 playerDirector,playerDirector 处理消息后将反馈发送给其他 player。
      * 方案二、在 playerDirector 中开放一些接收消息的接口,各 player 可以直接调用该接口来给playerDirector 发送消息,
      * player 只需传递一个参数给 playerDirector,这个参数的目的是使 playerDirector 可以识别发送者。同样,
      * playerDirector 接收到消息之后会将处理结果反馈给其他 player。
      */
      var playerDirector = (function () {
        var players = {} // 保存玩家的信息
        var 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 // 玩家队伍颜色
          var 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 // 玩家队伍颜色
          var teamPlayers = players[teamColor] // 玩家所在的队伍成员
          var all_dead = true // 是否全部死亡,默认是

          for (var i = 0, player; player = teamPlayers[i++];) { // 遍历检测队伍玩家是否全部死亡
            if (player.state !== 'dead') {
              all_dead = false
              break
            }
          }
          if (all_dead === true) { // 队伍玩家全部死亡
            for (var i = 0, player; player = teamPlayers[i++];) {
              player.lose() // 本队伍所有玩家 lose
            }
            for (var color in players) { // 其他队伍
              if (color !== teamColor) {
                var teamPlayers = players[color] // 其他队伍玩家
                for (var i = 0, player; player = teamPlayers[i++];){
                  player.win() // 其他队伍玩家 win
                }
              }
            }
          }
        }

        var reciveMessage = function () { // 接收的消息
          var message = Array.prototype.shift.call(arguments) // arguments 第一个参数为消息名称
          operations[message].apply(this, arguments) // 执行相应消息名称的操作
        }

        return { // 开放接口
          reciveMessage: reciveMessage
        }
      })()
      
      // 例子
      var player1 = playerFactory( 'player1 ', 'red' ) // 红队
      var player2 = playerFactory( 'player2 ', 'red' )
      var player3 = playerFactory( 'player3 ', 'red' )
      var player4 = playerFactory( 'player4 ', 'red' )

      var player5 = playerFactory( 'player5 ', 'blue' ) // 蓝队
      var player6 = playerFactory( 'player6 ', 'blue' )
      var player7 = playerFactory( 'player7 ', 'blue' )
      var player8 = playerFactory( 'player8 ', 'blue' )

      // 例子 输出:
      // player3 lost
      // player4 lost
      // player5 won
      // player6 won
      // player7 won
      // player8 won
      // player1 won
      player1.changeTeam('blue')
      player2.remove()
      player3.die()
      player4.die()

装饰者模式

定义: 给对象动态地增加职责的方式称为装 饰者(decorator)模式。

**说明:**装饰者模式能够在不改 变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种 更轻便灵活的做法,这是一种“即用即付”的 方式。代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确 定对象的全部功能时。代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。

核心代码&&例子:

    /*
     * 装饰者模式
     * 飞机大战发射不同子弹的例子
     */
     var Plane = function () {} // 飞机类 
     Plane.prototype.fire = function () { // 开火
       console.log('发射普通子弹')
     }

     var MissileDecorator = function (plane) { // 导弹装饰者
      this.plane = plane
     }
     MissileDecorator.prototype.fire = function () {
       this.plane.fire()
       console.log('发射导弹')
     }

     var AtomDecorator = function (plane) { // 原子弹装饰者
      this.plane = plane
     }
     AtomDecorator.prototype.fire = function () {
       this.plane.fire()
       console.log('发射原子弹')
     }

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

核心代码&&例子: AOP 装饰函数

/*
     * 装饰者模式
     * AOP 装饰函数
     * Function.prototype.before 方法和 Function.prototype.after 方法
     */
     Function.prototype.before = function (beforeFn) { // 在方法之前执行函数
      var _self = this // 保存原函数的引用
      return function () { // 返回包含原函数和新函数的“代理”函数
        beforeFn.apply(this, arguments) // 执行新函数
        return _self.apply(this, arguments) // 执行原函数
      }
     }

     Function.prototype.after = function (afterFn) { // 在方法之后执行函数
      var _self = this
      return function () {
        var ret = _self.apply(this, arguments)
        afterFn.apply(this, arguments)
        return ret
      }
     }

     var fn1 = function () {
       console.log('fn1')
     }
     fn1.before(function () {
       console.log('fn2')
     }).after(function () {
       console.log('fn3')
     })() // 输出: fn2 fn1 fn3

核心代码&&例子:

上面的 AOP 实现是在 Function.prototype 上添加 before after 方法,但许 多人不喜欢这种污染原型的方式,那么我们可以做一些变通,把原函数和新函数都作为参数传入before 或者 after 方法

    /*
     * 装饰者模式
     * 非 AOP 实现类似 Function.prototype.before 和 Function.prototype.after 方法
     */
     var before = function (fn, beforeFn) { // 在 fn 之前执行
      return function () {
        beforeFn.apply(this, arguments)
        return fn.apply(this, arguments)
      }
     }

     var after = function (fn, afterFn) { // 在 fn 之后执行 
      return function () {
        var ret = fn.apply(this, arguments)
        afterFn.apply(this, arguments)
        return ret
      }
     }

     // 例子
     var fn1 = function () {
       console.log('fn1')
     }
     var fn2 = function () {
       console.log('fn2')
     }
     var fn3 = function () {
       console.log('fn3')
     }
     after(before(fn1, fn2), fn3)() // fn2 fn1 fn3

状态模式

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

说明: 状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系, 所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情 发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。

使用场景: 电灯的状态控制,文件上传的状态控制

核心代码&&例子:

    /*
     * 状态模式
     * 电灯的例子
     */

    var State = function () {} // State抽象父类
    State.prototype.buttonWasPressed = function () {
      throw new Error('父类的 buttonWasPressed 方法必须被重写')
    }

     // 状态转换顺序: 弱光 -> 强光 -> 关灯 关灯 -> 弱光 不停循环
    var OffLightState = function (light) { // 关灯状态
      this.light = light
    }
    OffLightState.prototype = new State() // 继承 State抽象父类
    OffLightState.prototype.buttonWasPressed = function () { // 按钮事件 
      console.log('弱光')
      this.light.setState(this.light.weakLightState) // 切换到弱光状态
    }

    var WeakLightState = function (light) { // 弱光状态
      this.light = light
    }
    WeakLightState.prototype = new State() // 继承 State抽象父类
    WeakLightState.prototype.buttonWasPressed = function () { // 按钮事件 
      console.log('强光')
      this.light.setState(this.light.strongLightState) // 切换到强光状态
    }

    var StrongLightState = function (light) { // 强光状态
      this.light = light
    }
    StrongLightState.prototype = new State() // 继承 State抽象父类
    StrongLightState.prototype.buttonWasPressed = function () { // 按钮事件 
      console.log('关灯')
      this.light.setState(this.light.offLightState) // 切换到关灯状态
    }

    var Light = function () { // Light 类
      this.offLightState = new OffLightState(this)
      this.weakLightState = new WeakLightState(this)
      this.strongLightState = new StrongLightState(this)
      this.button = null
    }

    Light.prototype.init = function () { // 初始化方法
      var button = document.createElement('button')
      var _self = this
      this.button = document.body.appendChild(button)
      this.button.innerHTML = '开关'
      this.currentState = this.offLightState // 设置当前默认状态
      this.button.onclick = function () { // 定义用户请求动作
        _self.currentState.buttonWasPressed()
      }
    }
    Light.prototype.setState = function (newState) { // 设置状态方法
      this.currentState = newState
    }

    // 例子
    var light = new Light()
    light.init()

核心代码&&例子:

    /*
     * 状态模式
     * 文件上传例子
     */
     // 提供 window.external.upload 函数,模拟创建上传插件
     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) // 设置初始状态 waiting
       this.uploadingState = new UploadingState(this)
       this.pauseState = new PauseState(this)
       this.doneState = new DoneState(this)
       this.errorState = new ErrorState(this)
       this.currState = this.signState // 设置当前状态
     }

     // Upload.prototype.init 方法负责往页面中创建跟上传流程有关的 DOM 节点,并开始绑定按钮的事件
     Upload.prototype.init = function () {
       var that = this
       this.dom = document.createElement('div')
       this.dom.innerHTML = `<span>文件名称:${this.fileName}</span>
                            <button data-action="button1">扫描中</button>
                            <button data-action="button2">删除</button>`
       document.body.appendChild(this.dom)
       this.button1 = this.dom.querySelector('[data-action = "button1"]')
       this.button2 = this.dom.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 类中
     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)
     }

     // 我们要编写各个状态类的实现。值得注意的是,我们使用了 StateFactory ,从而避免因为 JavaScript 中没有抽象类所带来的问题
     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 Fn = function (uploadObj) {
          this.uploadObj = uploadObj
        }
        Fn.prototype = new State()
        for (var i in param) {
          Fn.prototype[i] = param[i]
        }
        return Fn
      }
     })()
    
    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(function () {
      window.external.upload('uploading') // 一秒后开始上传
    }, 1000)
    setTimeout(function () {
      window.external.upload('done') // 五秒后开始上传
    }, 5000)

适配器模式

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

使用场景: 接口数据的适配,接口的适配

核心代码&&例子:

    /*
     * 适配器模式
     * 适配第三方方法名
     */
     // renderMap 需要的方法名是 show() ,而第三方给的却不是
     var renderMap = function (map) { // 渲染地图
       if (map.show instanceof Function) {
         map.show()
       }
     }

     var googleMap = {
       show: function () {
         console.log('开始渲染谷歌地图')
       }
     }
     var baiduMap = {
       display: function () {
         console.log('开始渲染百度地图')
       }
     }

     var baiduMapAdapter = { // 百度地图适配器
       show: function () {
         return baiduMap.display()
       }
     }

     // 测试
     renderMap(googleMap) // 开始渲染谷歌地图
     renderMap(baiduMapAdapter) // 开始渲染百度地图

引用

  • 《JavaScript设计模式与开发实践》 曾探 著