Skip to content

Latest commit

 

History

History
1693 lines (1236 loc) · 51.7 KB

JS设计模式.md

File metadata and controls

1693 lines (1236 loc) · 51.7 KB

设计模式

概念

什么是模式

模式是一种可复用的解决方案,可用于解决软件设计中遇到的常见问题。

模式的优点

  • 复用模式有助于防止在应用程序开发过程中小问题引发大问题
  • 模式可以提供通用的解决方案
  • 某些模式可以通过避免代码复用减少代码的总体资源占用量
  • 模式会使开发沟通更快速
  • 模式可以逐步改进

设计模式的结构

  • 模式名称
  • 描述
  • 上下文大纲
  • 问题陈述
  • 解决方案
  • 设计
  • 实现
  • 插图
  • 示例
  • 辅助条件
  • 关系
  • 已知的用法
  • 讨论

反模式

JavaScript中的反模式示例如下

  • 在全局上下文中定义大量的变量污染全局命名空间
  • 向setTimeout或setInterval传递字符串,而不是函数
  • 修改Object类的原型
  • 以内联形式使用JavaScript
  • 使用document.write

设计模式的类别

创建型

专注于处理对象创建机制,以适合给定情况的方式来创建对象。创建对象的基本方法可能导致项目复杂性增加,而创建型模式旨在通过控制创建过程来解决这种问题。

设计模式 描述
Constructor(构造器)
Factory(工厂)
Abstract(抽象)
Prototype(原型)
Singleton(单例)
Singleton(单例)
Builder(生成器)

结构型

结构型与对象组合有关,通常可以用于找出在不同对象之间建立关系的简单方法。这种模式有助于确保在系统某一部分发生变化时,系统的整个结构不需要同时改变,同时对于不适合某一特定目的而改变的系统部分,也能够完成重组。

设计模式 描述
Iterator(装饰者)
Facade(外观)
Flyweight(享元)
Adapter(适配器)
Proxy(代理)

行为

行为设计模式专注于改善或简化系统中不同对象之间的通信。

设计模式 描述
Iterator(迭代器)
Mediator(中介者)
Observer(观察者)
Visitor(访问者)

Constructor(构造器)模式

function Person() {}
var person = new Person()

// 带原型的Constructor
Person.prototype.getName = {}

Module(模块)模式

  • 对象字面量表示法
  • Module模式
  • AMD模式
  • CommonJS模块
  • ECMAScript Harmony模块

对象字面量表示法

let person = {
  name: '',
  age: 0,
  getName: function() {},
  getAge: function() {}
}

Module模式

Module模式可以为类提供私有和公有的方法,Module模式会封装一个作用域,从而屏蔽来自全局作用域的特殊部分,使一个单独的对象拥有公有/私有的方法和变量。

Module模式使用闭包封装私有状态和组织,提供一种包装混合公有/私有方法和变量的方式,防止其泄露至全局作用域,防止与全局作用域中的方法和变量发生冲突。通过该模式只需返回一个公有API,而其他的一切则都维持在私有闭包里。

Module模式可以屏蔽处理底层事件逻辑,只暴露供应用程序调用的公有API,该模式返回的是一个对象而不是函数。

// 一个立即执行的匿名函数创建了一个作用域
// 全局作用域无法获取私有变量_counter
var moduleMode = (function() {
  // 私有变量
  var _counter = 0

  // 返回一个公有对象
  return {
    // 公有API
    increment: function() {
      return ++_counter
    },
    // 公有API
    reset: function() {
      _counter = 0
      return _counter
    }
  }
})()

let counter = moduleMode.increment()
console.log(counter)
counter = moduleMode.increment()
console.log(counter)
counter = moduleMode.reset()
console.log(counter)
// _counter is not defined
console.log(_counter)

Module模式的本质是使用函数作用域来模拟私有变量,在模式内,由于闭包的存在,声明的变量和方法旨在改模式内部可用,但在返回对象上定义的变量和方法,则对外部使用者可用。

Module模式也可用于命名空间

 var Namespace = (function() {
  // 私有变量
  var _counter = 0

  // 私有方法
  var _sayCounter = function() {
    console.log(_counter)
  }

  // 返回一个公有对象
  return {
    // 公有变量
    counter: 10,

    // 公有API
    increment: function() {
       ++_counter
       // 调用私有变量
       _sayCounter()
       return _counter
    },
    // 公有API
    reset: function() {
      _counter = 0
      _sayCounter()
      return _counter
    }
  }
})()

Namespace.increment()
Namespace.increment()
Namespace.reset()

Module模式的变化

引入

可以使jQuery、Underscore作为参数引入模块

var moduleMode = (function($, _) {
  function _method() {
    $('.container').html('test')
  }

  return {
    method: function() {
      _method()
    }
  }
})($,_)

moduleMode.method()

引出

let moduleMode = (function() {
  var public = {},
      _private = 'hello'

  function _method() {
    console.log(_private)
  }

  public.name = 'public'

  public.method = function () {
    _method()
  }

  return public
})()

console.log(moduleMode.name)

Module模式的优缺点

  • 优点:整洁、支持私有数据。
  • 缺陷:私有数据难以维护(想改变可见性需要修改每一个使用该私有数据的地方),无法为私有成员创建自动化单元测试,开发人员无法轻易扩展私有方法。

Singleton(单例/单体)模式

单例模式作为一个静态的实例实现时,可以延迟创建实例,从而在没有使用该静态实例之前,无需占用浏览器的资源或内存。同时当在系统中确实需要一个对象来协调其他对象时,Singleton模式非常有用。单例模式可以推迟初始化,通常是因为它需要一些信息,这些信息在初始化期间可能无法获得。

var Singleton = (function() {
  function Single(options) {
    options = options || {}
    this.name = options.name || 'Single'
    this.age = options.age || 1
  }

  var _instance

  // 返回一个闭包,_instance不会被销毁
  return {
    name: 'Singleton',
    getInstance: function(options) {
      if(!_instance) {
        _instance = new Single(options)
      }
      return _instance
    }
  }
})()


var single = Singleton.getInstance({
  name: 'ziyi2',
  age: 28
})

// Single {name: "ziyi2", age: 28}
console.log(single)

Observer(观察者)模式

观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变更自动通知给这一系列观察者对象。当subject目标对象需要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。

一个或多个观察者对目标对象的状态感兴趣时,可以将自己依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每个观察者的更新方法。如果观察者对目标对象的状态不感兴趣,也可以将自己从中分离。

对象 描述
Subject(目标) 维护一系列的观察者,方便添加、删除和通知观察者
Observer(观察者) 为那些目标状态发生改变时需要通知的对象提供一个更新接口
subject(目标)实例对象 状态发生变化时通知观察者实例对象们更新状态
observer(观察者)实例对象 实现更新接口用于更新状态

观察者设计模式

观察者列表对象

观察者列表对象用于维护一系列的观察者实例对象

// 观察者列表对象
function ObserverList() {
  this.observerList = []
}

// 增加观察者实例对象
ObserverList.prototype.add = function(observer) {
  return this.observerList.push(observer)
}

// 查看观察者实例对象的数量
ObserverList.prototype.getCount = function() {
  return this.observerList.length
}

// 获取某个观察者实例对象
ObserverList.prototype.get = function(index) {
  if(index < -1 || index > this.observerList.length) return
  return this.observerList[index]
}

// 删改观察者实例对象的列表 省略...

Subject(目标)对象

使用观察者列表对象维护观察者实例对象,并可以通知观察者实例对象更新状态

// 目标对象(在观察者列表上增、删观察者实例对象)
function Subject() {
  this.observers = new ObserverList()
}

// 增加观察者实例对象
Subject.prototype.add = function(observer) {
  this.observers.add(observer)
}

// 通知观察者列表更新
Subject.prototype.notify = function(context) {
  var count = this.observers.getCount()
  for(var i=0; i<count; i++) {
    this.observers.get(i).update(context)
  }
}

Observer(观察者)对象

主要用于提供更新接口

function Observer() {
  // 更新目标实例对象的状态的接口
  this.update = function() {}
}

扩展对象

扩展对象用于扩展观察者实例对象和目标实例对象

// obj -> 观察者实例对象或目标实例对象
// extension -> 需要被扩展的对象
function extend(obj, extension) {
  for(var key in obj) {
    extension[key] = obj[key]
  }
}

具体的DOM元素用于创建观察者实例对象和目标实例对象

创建DOM元素,用于扩展观察者实例对象和目标实例对象

<button id="btn">添加新的观察者实例对象</button>
<!-- 目标实例对象 -->
<input type="checkbox"  id="checkbox">
<!-- 新创建的观察者实例对象的容器 -->
<div id="div"></div>

创建目标实例对象

// 获取checkbox元素
var checkbox = document.getElementById('checkbox')

// 创建具体目标实例,并绑定到checkbox元素上
extend(new Subject(), checkbox)

// 点击checkbox会触发目标实例对象的通知方法
// 广播到所有观察者实例对象促使它们调用更新状态方法
checkbox.onclick = function() {
  checkbox.notify(checkbox.checked)
}

创建观察者实例对象

// 获取btn和div元素
var btn = document.getElementById('btn'),
    div = document.getElementById('div')

btn.onclick = handlerClick

function handlerClick() {
  // 创建checkbox元素(注意和目标实例对象不同)
  var input = document.createElement("input")
  input.type = "checkbox"

  // 创建具体的观察者实例,并绑定到checkbox元素上
  extend(new Observer(), input)

  // 重写观察者更新行为
  input.update = function(value) {
    this.checked = value
  }

  // 通过目标实例对象新增需要被广播的观察者实例对象
  checkbox.add(input)

  // 将观察者附到div元素上
  div.appendChild(input)
}

至此,通过按钮新增观察者实例对象,点击目标checkbox实例对象时,checkbox的状态会广播给所有新增的观察者实例对象checkbox,从而使目标实例对象的值和观察者实例对象的值保持一致,实现了观察者模式。

Publish/Subscribe(发布/订阅)模式

发布/订阅设计模式

需要注意token是每一次订阅的唯一标识,通过token可以取消特定的频道订阅。

// 发布/订阅模式
var pubsub = (function() {

  // 订阅和发布的事件频道集(桥梁、中间带)
  var _channels = [],
      _subUid = -1

  return {
    // 订阅频道
    subscribe: function(channel, handler) {
      if(!_channels[channel]) _channels[channel] = []
      var token = (++_subUid).toString()
      _channels[channel].push({
        token: token,
        handler: handler
      })
      return token
    },

    // 广播频道
    publish: function(channel, data) {
      if(!_channels[channel]) return false
      // 获取频道订阅者
      var subscribers = _channels[channel]
      var len = subscribers.length
      // 后订阅先触发
      while(len--) {
        subscribers[len].handler(data, channel, subscribers[len].token)
      }
      return this
    },

    // 移除订阅
    unsubscribe: function(token) {
      for(var channel in _channels) {
        var len = _channels[channel].length
        for(var index=0; index<len; index++) {
          if(_channels[channel][index].token === token) {
            _channels[channel].splice(index, 1)
            return token
          }
        }
      }
    }
  }
})()


// 订阅频道0
var token = pubsub.subscribe('channel0', function(data, channel, token) {
  console.log('channel: ' +  channel +' data: ', data + ' token: ' + token)
})

// 订阅频道0
pubsub.subscribe('channel0', function(data, channel, token) {
  console.log('channel: ' +  channel +' data: ', data + ' token: ' + token)
})

// 广播频道0
pubsub.publish('channel0', {
  name: 'ziyi2',
  age: 28
})

// 取消某个特定频道0的订阅
pubsub.unsubscribe(token)

// 继续广播频道0
pubsub.publish('channel0', {
  name: 'ziyi2',
  age: 28
})

Observer(观察者)模式和Publish/Subscribe(发布/订阅)模式的区别

观察者和发布/订阅模式对比

Publish/Subscribe(发布/订阅)模式使用一个主题/事件通道,这个通道介于订阅者和发布者之间,该设计模式允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者需要的信息,采用事件通道可以避免发布者和订阅者之间产生依赖关系。

需要注意的是,Observer(观察者)模式允许观察者实例对象(订阅者)执行适当的事件处理程序来注册和接收目标实例对象(发布者)发出的通知(即在观察者实例对象上注册update方法),使订阅者和发布者之间产生了依赖关系,且没有事件通道。

优点

解耦,可用于设计分层以及分层之间的通信,可用于将应用程序分解为更小、更松散耦合的块,改进代码的管理和潜在复用,是设计解耦性系统的最佳模式。

缺陷

具有一定的不稳定性,发布和订阅都是无状态的,发布无法知道订阅的运行情况。

Mediator(中介者)模式

提供统一的公开接口,系统的不同部分可以通过该接口进行通信。中介者模式类似于信息中转站,例如系统的各个组件可以通过这个中心控制点(中介者模式)进行通信,而不是彼此引用,这种模式可以帮助解耦系统并提高组件的可重用性。

// 中介者模式
var mediator = (function() {

  var _channels = [],
      _subUid = -1

  function subscribe(channel, handler) {
    if(!_channels[channel]) _channels[channel] = []
    var token = (++_subUid).toString()
    _channels[channel].push({
      token: token,
      context: this,
      handler: handler
    })
    return token
  }   
 
  function publish(channel, data) {
    if(!_channels[channel]) return false
    var subscribers = _channels[channel]
    var len = subscribers.length
    while(len--) {
      subscribers[len].handler.call(subscribers[len].context, data, channel, subscribers[len].token)
    }
    return this
  }

  function unsubscribe(token) {
    for(var channel in _channels) {
      var len = _channels[channel].length
      for(var index=0; index<len; index++) {
        if(_channels[channel][index].token === token) {
          _channels[channel].splice(index, 1)
          return token
        }
      }
    }
  }

  // Module模式引出
  return {
    subscribe: subscribe,
    publish: publish,
    unsubscribe: unsubscribe,
    // 绑定到其他对象使用该设计模式
    installTo: function(obj) {
      obj.subscribe = subscribe
      obj.publish = publish 
      obj.unsubscribe = unsubscribe
    }
  }
})()

mediator.subscribe('message', function(data, channel, token) {
  // true
  console.log(this === mediator) 
  console.log(data)
  console.log(channel)
  console.log(token)
})

mediator.publish('message', 'hello world')

Mediator(中介者)模式和Observer(观察者)模式

Mediator(中介者)模式:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。通信可以实现双向。 Observer(观察者)模式:不存在封装约束的单一对象,目标对象和观察者对象必须合作才能维持约束。 观察者对象向订阅它们的对象发布其感兴趣的事件。通信只能是单向的。

Prototype(原型)模式

原型模式可以让多个构造函数对应的实例对象共享同一个原型对象的属性和方法,具体查看ES5中类的继承

如果创建实例对象的构造函数相对复杂,耗时较长,此时可以不用new关键字去复制这些基类,可以通过这些对象属性或方法进行复制来实现创造,这是原型模式最初的思想,通过原型继承的特点,首先创建一个原型模式的对象复制方法

// 基于已经存在的模板对象克隆新对象
// 需要注意模板引用类型的属性进行了浅复制
// 因此模板对象中的数组类型数据会被所有实例对象共享引用
function prototypeExtend() {
  var F = function() {},
      args = arguments,
      i = 0,
      len = arguments.length

  for(; i<len; i++) {
    for(var key in arguments[i]) {
      F.prototype[key] = arguments[i][key]
    }
  }   
  return new F()
}


let person = prototypeExtend({
  name: '111',
  age: 28
}, {
  getName: function() {
    return this.name
  }
}, {
  getAge: function() {
    return this.age
  }
})

console.log(person.name)
console.log(person.getName())

Command(命令)模式

命令模式将命令执行者和命令发起者解耦,提供更大的整体灵活性。用类来做比喻就是,抽象类(类似于命令发起者)定义一个接口,但不为它的成员函数提供实现,作为一个基类派生出其他类,派生类(类似于命令执行者)首先具体接口。

命令模式主要思想是提供一种分离职责的手段,这样可以做到松耦合。假如不采用命令模式,直接调用命令执行者的API,一旦这些API发生改变,那么要求程序里所有访问这些API的命令执行者都需要进行修改,从而被视为一个耦合层。

// 命令模式
var command = (function() {
  // 命令执行者
  var action = {
    create: function(data) {
      console.log('create command fired: ', data)
    },

    destroy: function(data) {
      console.log('destroy command fired: ', data)
    }
  }

  // 命令发起者
  return function run(name) {
    return action[name] && action[name].apply(action, [].slice.call(arguments, 1))
  }
})()

command('create', { name: 'ziyi2', age: 28 })
command('destroy', { name: 'ziyi2', age: 28 })

Facade(外观)模式

为复杂的子系统接口提供更高级的统一接口,通过这个接口使得对子系统接口的访问更容易,在JavaScript中有时也会用于对底层结构兼容性做统一封装来简化用户使用。

var Browser = {
  event: {
    add: function(dom, type, fn) {
      if(dom.addEventListener) {
        dom.addEventListener(type, fn, false)
      } else if(dom.attachEvent) {
        dom.attachEvent('on'+type, fn)
      } else {
        dom['on'+ type] = fn
      }
    },

    remove: function() {
      // ...
    },

    self: function(event) {
      return event || window.event
    },

    target: function(event) {
      return event.target || event.srcElement
    },

    preventDefault: function(event) {
      let event = this.event.self(event)
      event.preventDefault 
      ? event.preventDefault() 
      : event.returnValue = false
    }
  },

  id: function(dom, id) {
    return dom.getElementById(id)
  },

  html: function(dom, id, html) {
    this.id(dom, id).innerHTML = html
  }
}

通过外观模式对接口的二次封装隐藏其复杂性,可以简化用户的使用,外观模式也可以结合Module(模块)模式使用,需要注意的是外观模式会产生隐性成本,在设计时需要衡量是否需要使用外观模式抽象和封装某些结构。

Factory(工厂)模式

简单工厂模式

简单工厂模式(静态工厂方法)主要用于创建同一类对象。该模式通过创建一个新对象然后包装增强其属性和功能实现,需要注意的是使用该模式它们的方法不能共享,不能像原型继承那样共享原型方法。

function createPerson(type, name) {
  var person
  var person = new Object()
  person.name = name
  person.getName = function() {
    return this.name
  }
  return person
}

let person1 = createPerson('father', 'ziyi1')
let person2 = createPerson('children', 'ziyi2')

也可以增强简单工厂模式, 使其可以创建不同类的对象

function createPerson(type, name) {
  
  var person

  switch(type) {
    case 'father':
      // 父亲差异部分
      // 例如 person = new Father(name)
      break
    case 'mother':
      // 母亲差异部分
      // 例如 person = new Mother(name)
      break   
    case 'children':
      // 孩子差异部分
      // 例如 person = new Children(name)
      break
    default:
      break
  }
  return person
}

工厂方法模式

简单工厂模式创建多类对象相对不够灵活,工厂方法模式可以轻松创建多个类的实例对象

function Person(type, name, age, job) {
  // 安全模式,外部可以不使用new关键字
  if(this instanceof Person) {
    return new this[type](name, age, job)
  } else {
    return new Person(type, name, age, job)
  }
}
 
Person.prototype = {
  constructor: Person,
  // Father类
  Father: function(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    console.log(`Father: ${name},${age},${job}`)
  },

  // Mother类
  Mother: function(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    console.log(`Mother: ${name},${age},${job}`)
  },

  // Children类
  Children: function(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    console.log(`Children: ${name},${age},${job}`)
  }
}


let data = [{
  type: 'Father',
  name: 'ziyi2',
  age: 44,
  job: 'web'
}, {
  type: 'Mother',
  name: 'ziyi2',
  age: 280,
  job: 'web'
}, {
  type: 'Children',
  name: 'ziyi2',
  job: 'web'
}]

for(let person of data) {
  Person(person.type, person.name, person.age, person.job)
}

在项目中通过对产品类的抽象使其创建业务主要负责用于创建多类产品的实例。

抽象工厂模式

抽象类是一种声明但是不能使用的类,JavaScript中有一个保留的关键字abstract对应抽象概念,抽象类在实例化时应该抛出错误。抽象类主要用于定义产品簇,并声明一些必备的方法,如果子类没有重写这些方法,那么子类实例化后调用这些方法时应该抛出错误。

抽象类中定义的方法只是显性的定义一些功能,但没有具体的实现,不能使用抽象类创建真实的使用实例对象。

function objectCreate(o) {
  function F() {}
  F.prototype = o
  return new F()
}
                                                                                                
/** 
 * @Author: zhuxiankang 
 * @Date:   2018-06-14 09:17:50  
 * @Desc:   抽象工厂方法
 * @Parm:   subClass -> 子类
 *          abstractClass -> 抽象类 
 */
function AbstractFactory (subClass, abstractClass) {
  if(typeof AbstractFactory[abstractClass] == 'function') {
    // 注意和寄生组合式继承中var prototype = objectCreate(AbstractFactory[abstractClass].prototype)的区别
    // 这里是不仅继承了抽象类的原型对象的方法
    // 还继承了抽象类的实例对象的方法和属性
    // 继承构造函数中没有继承抽象类的实例对象的方法和属性
    var prototype = objectCreate(new AbstractFactory[abstractClass]())
    prototype.constructor = subClass
    subClass.prototype = prototype
  } else {
    throw new Error(abstractClass + ' undefined!')
  }
}

// Person抽象类
AbstractFactory.Person = function() {
  // 实例属性会被子类继承
  this.type = 'person'
}

// Person抽象类声明的抽象方法(抽象方法不能被Person抽象类的实例使用)
AbstractFactory.Person.prototype = {
  // constructor: AbstractFactory.Person,

  getType: function() {
    return new Error('abstract method "getType" can not be called!')
  },

  getName: function() {
    return new Error('abstract method "getName" can not be called!')
  }
}

// 创建其他抽象类
// AbstractFactory.?.prototype


// 创建继承抽象类的子类
function Father(name) {
  this.name = name
}


function Mother(name) {
  this.name = name
}

// 抽象工厂方法实现对抽象类的继承
AbstractFactory(Father, 'Person')
AbstractFactory(Mother, 'Person')


// 需要注意放在AbstractFactory工厂方法之后
// 因为工厂方法里重写了子类的prototype原型对象
Mother.prototype.getType = function() {
  return this.type
}


var father = new Father('ziyi2')
// person
console.log(father.type) 
// Error: abstract method "getType" can not be called!
console.log(father.getType())

var mother = new Mother('ziyi2')
// person 根据原型链,子类的该方法重写了从父类继承的方法
console.log(mother.getType())

关于寄生组合式继承请查看js类和继承。抽象工厂模式中的抽象类创建的不是一个真实的对象实例,而是一个类簇,抽象类指定了类的结构,区别于简单工厂模式创建单一对象,工厂方法模式创建多类对象。不过这种模式应用的并不广泛,因为JavaScript中不支持抽象化创建于虚拟方法。

Mixin(混入)模式

Mixin是可以轻松被一个子类或一组子类轻松继承功能的类,目的是函数复用。

继承Mixin

在JavaScript中,可以使用原型链轻松实现继承Mixin,具体可参考寄生组合式继承,Mixin类的方法和属性可以轻松被子类继承。

Mixin(混入)

Mixin允许对象通过较低的复杂性借用(继承)功能。继承Mixin可以实现父类的属性和方法被多个子类继承,但是在复杂的业务场景中可能存在一个子类需要继承多个父类的情况。

Mixin对象可以被视为具有可以在很多其他对象原型中轻松共享属性和方法的对象。

// mixin 混入对象
// extend 被混入的对象
var mixins = function(extend, mixin) {
  // 指定特定的混入属性
  if(arguments[2]) {
    for(var i=2,len=arguments.length; i<len; i++) {
      if(!mixin[arguments[i]]) continue
      extend[arguments[i]] = mixin[arguments[i]]
    }
  // 混入全部
  } else {
    for(var key in mixin) {
      // 被混入对象存在同名属性则不混入
      if(!extend[key]) {
        extend[key] = mixin[key]
      }
    }
  }
}


// name混入对象
var personName = {
  getName: function() {
    return this.name
  },

  setName: function(name) {
    this.name = name
  }
}


// age混入对象
var personAge = {
  getAge: function() {
    return this.age
  },

  setAge: function(age) {
    this.age = age
  }
}

// 类
function Person(name, age) {
  this.name = name
  this.age = age
}

mixins(Person.prototype, personName, 'getName')
mixins(Person.prototype, personAge)

var person = new Person('ziyi2', 11)
// Person {name: "ziyi2", age: 11}
// age:11
// name:"ziyi2"
// __proto__:
// getAge:ƒ ()
// getName:ƒ ()
// setAge:ƒ (age)
// constructor:ƒ Person(name, age)
console.log(person)

Mixin有助于减少系统中的重复功能及增加函数复用,但是也存在一些缺陷,将Mixin导入对象原型会导致函数起源方面的不确定性以及原型污染。

装饰者模式

装饰者模式旨在促进代码复用,提供了将行为添加至系统现有的类的功能,相对于类原有的基本功能来说不是必要的,如果是必要的,可以被合并到父类。装饰者在不改变原有对象的基本功能的基础上,对其功能进行扩展。

function MacBook() {
  this.cost = function() {
    return 997
  }
  this.screenSize = function() {
    return 11.6
  }
}


function Memory(macbook) {
  var v = macbook.cost()
  macbook.cost = function() {
    return v + 75
  }
}

function Engraving(macbook) {
  var v = macbook.cost()
  macbook.cost = function() {
    return v +200
  }
}


var macbook = new MacBook()
Memory(macbook)
Engraving(macbook)
//1272
console.log(macbook.cost()) 

如果使用ES6语法,具体可以查看ES6中的装饰者。装饰者模式如果管理不当,会极大的复杂化应用程序架构,因为向命名空间中引入了很多小型但类似的对象。

var decorator = function(dom, fn) {
  if(typeof dom.onclick === 'function') {
    // 获取原有的点击事件
    var origin = dom.onclick
    // 装饰dom的点击事件
    dom.onclick = function(event) {
      // 保留原有事件
      origin.call(dom, event)
      // 装饰新事件
      fn.call(dom, event)
    }
  } else {
    dom.onclick = fn()
  }
}


var btn = document.getElementById('btn')
// 原有的点击事件
btn.onclick = function(event) {
  console.log('origin btn click')
}

function fn(event) {
  console.log('decorator btn click')
}

// 装饰btn
decorator(btn, fn)

按钮原有的点击事件功能不变,在此基础上对按钮的点击功能进行了装饰。

Flyweight(享元)模式

// 享元接口
var Flyweight = {
  serverName: function() {},
  getName: function() {}
}

// 享元具体实现
function ConcreteFlyweight(newName) {
  var _name = newName

  // 如果已经为某一功能定义接口,则实现该功能
  if(typeof this.getName === "function") {
    this.getName = function() {
      return _name
    }
  }

  if(typeof this.serverName === "function") {
    this.serverName = function(context) {
      console.log('Server name ' + _name + ' to table number ' + context.getTable())
    }
  }
}


// 实现接口方法
Function.prototype.implementsFor = function(Interface) {
  if(Interface.constructor === Function) {
    this.prototype = new Interface()
    this.prototype.parent = Interface.prototype
  } else {
    this.prototype = Interface
    this.prototype.parent = Interface
  }
  this.prototype.constructor = this
}

// 实现接口
ConcreteFlyweight.implementsFor(Flyweight)


function TableContext(tableNumber) {
  return {
    getTable: function() {
      return tableNumber
    }
  }
}


// 享元工厂
function FlyweightFactory() {
  var _names = []

  return {
    getName: function(name) {
      _name = new ConcreteFlyweight(name)
      _names.push(_name)
      return _name
    },

    getTotal: function() {
      return _names.length
    }
  }
}


// 示例
var names = new ConcreteFlyweight()
var tables = new TableContext()

var orders = 0
var flyweightFactory
function takeOrder(name, table) {
  names[orders] = flyweightFactory.getName(name)
  tables[orders ++] = new TableContext(table)
}

flyweightFactory = new FlyweightFactory()
 
takeOrder('ziyi2', 1)
takeOrder('ziyi2', 2)

takeOrder('ply', 1)
takeOrder('ply', 4)

for(var i=0; i<orders; i++) {
  names[i].serverName(tables[i])
}
// Server name ziyi2 to table number 1
// Server name ziyi2 to table number 2
// Server name ply to table number 1
// Server name ply to table number 4

console.log(flyweightFactory.getTotal())
// 4

享元模式

享元模式主要用于减少内存占用,对数据进行细化处理。本质是分离与共享,分离的是对象状态中的变与不变的部分,共享的是对象中不变的部分,把不变的部分作为享元对象的内部状态,而变化的部分则作为外部状态,由外部来维护。

把内部状态分离出来共享,称之为享元,通过共享享元对象来减少对内存的占用。外部状态分离出来,放到外部,让应用在使用的时候进行维护,并在需要的时候传递给享元对象使用。为了控制对内部状态的共享,并且让外部能简单地使用共享数据,提供一个工厂来管理享元,把它称为享元工厂。

享元模式的重点就在于分离变与不变。把一个对象的状态分成内部状态和外部状态,内部状态是不变的,外部状态是可变的。然后通过共享不变的部分,达到减少对象数量并节约内存的目的。

享元模式真正缓存和共享的数据是享元的内部状态,而外部状态是不应该被缓存共享的。

在享元模式中,为了创建和管理共享的享元部分,引入了享元工厂,享元工厂中一般都包含有享元对象的实例池,享元对象就是缓存在这个实例池中。所谓实例池,指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生存周期。

享元模式的调用顺序

  • 通过享元工厂来获取共享的享元对象
  • 创建相应的享元对象
  • 调用共享的享元对象的方法,传入外部状态

享元模式用于减少应用程序所需对象的数量。这是通过将对象的内部状态划分为内在数据(instrinsic data)和外在数据(extrinsic data)两类而实现的。内在数据是指类的内部方法所需要的信息。没有这种数据的话类就不能正常运行。外在数据则是可以从类身上剥离并存储在其外部的信息。我们可以将内在状态相同的所有对象替换为同一个共享对象,用这种方法可以把对象数量减少到不同内在状态的数量。

创建这种共享对象需要使用工厂,而不是普通的构造函数。这样做可以跟踪到已经实例化的各个对象,从而仅当所需对象的内在状态不同于已有对象时才创建一个新对象。对象的外在状态被曝存在一个管理器对象中。在调用对象的方法时,管理器会把这些外在状态作为参数传入。

假设要开发一个系统,用以代表一个城市的所有汽车。你需要保存每一辆汽车的详细情况(品牌,型号和出厂日期)及其所有权的详细情况(车主姓名,车牌号和最近登记日期)。当然,你决定把每辆汽车表示为一个对象:

/**
  * Car类
  * @param make 品牌
  * @param model 型号
  * @param year 出厂日期
  * @param owner 车主姓名
  * @param tag 车牌号
  * @param renewDate 最近登记日期
  * @constructor Car
  */
 
var Car = function (make, model, year, owner, tag, renewDate) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.owner = owner;
  this.tag = tag;
  this.renewDate = renewDate;
 };
 
Car.prototype = {
   getMake: function () {
     return this.make;
   },
   getModel: function () {
     return this.model;
   },
   getYear: function () {
     return this.year;
   },
   transferOwnership: function (newOwner, newTag, newRenewDate) {
     this.owner = newOwner;
     this.tag = newTag;
     this.renewDate = newRenewDate;
   },
   renewRegistration: function (newRenewDate) {
     this.renewDate = newRenewDate;
   },
   isRegistrationCurrent: function () {
     var today = new Date();
     return today.getTime() < Date.parse(this.renewDate);
   }
 };  

这个系统最初表现不错。但是随着城市人口的增长,你发现它一天天地变慢。数以十万计的汽车对象耗尽了可用的计算资源。要想优化这个系统,可以采用享元模式减少所需对象的数目。

优化工作的第一步是把内在状态与外在状态分开。

将对象数据划分为内在和外在部分的过程有一定的随意性。既要维持每个对象的模块性,又想把尽可能多的数据作为外在数据处理。划分依据的选择多少有些主观性。在本例中,车的自然数据(品牌,型号和出厂日期)属于内在数据,而所有权数据(车主姓名,车牌号和最近登记日期)则属于外在数据。这意味着对于品牌,型号和出厂日期的每一种组合,只需要一个汽车对象就行,这个数目还是不少,不过与之前相比已经少了几个数量级。每个品牌-型号=出厂日期组合对应的那个实例将被所有该类型汽车的车主共享。下面是新版Car类的代码:

var Car = function (make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
};

Car.prototype = {
     getMake: function () {
         return this.make;
     },
     getModel: function () {
         return this.model;
     },
     getYear: function () {
         return this.year;
     }
 };

上面的代码删除了所有外在数据。所有处理登记事宜的方法都被转移到一个管理其对象中(不过,也可以将这些方法留在原地,并为其增加对应于各种外在数据的参数)。因为现在对象的数据已被分为两大部分,所以必须用工厂来实例化它。

用工厂进行实例化,这个工厂很简单。它会检查之前是否已经创建过对应于指定品牌-型号-出厂日期组合的汽车,如果存在这样的汽车那就返回它,否则创建一辆新车,并把它包村起来供以后使用。这就确保了对应于每个唯一的内在状态,只会创建一个实例:

var CarFactory = (function () {
	 // 实例池
     var createdCars = {};
 
     return {
         /**
          * 工厂方法
          * @param make 品牌
          * @param model 型号
          * @param year 出厂日期
          * @return new Car()
          */
         createCar: function (make, model, year) {
             if (createdCars[make + '-' + model + '-' + year]) {
                 return createdCars[make + '-' + model + '-' + year];
             } else {
                 var car = new Car(make, model, year);
                 createdCars[make + '-' + model + '-' + year] = car;
                 return car;
             }
         }
     };
 })();

封装在管理器中的外在状态要完成这种优化还需要一个对象。所有那些从Car对象中删除的数据必须有个保存地点,我们用一个单体来做封装这些数据的管理器。原先的每一个Car对象现在都被分割为外在数据及其所属的共享汽车对象的引用这样两部分。Car对象与车主数据的组合称为汽车记录(car record)。管理器存储着这两方面的信息。它还包含着从原先的Car类删除的方法:

var CarRecordManager = (function () {
     var carRecordDatabase = {};
 
     return {
         // Add a new car record into the city's system
         addCarRecord: function (make, model, year, owner, tag, renewDate) {
             var car = CarFactory.createCar(make, model, year);
             carRecordDatabase[tag] = {
                 owner: owner,
                 renewDate: renewDate,
                 car: car
             };
         },
         // Methods previously contained in the Car class.
         transferOwnership: function (tag, newOwner, newTag, newRenewDate) {
             var record = carRecordDatabase[tag];
             record.owner = newOwner;
             record.tag = newTag;
             record.renewDate = newRenewDate;
         },
         renewRegistration: function (tag, newRenewDate) {
             carRecordDatabase[tag].renewDate = newRenewDate;
         },
         isRegistrationCurrent: function (tag) {
             var today = new Date();
             return today.getTime() < Date.parse(carRecordDatabase[tag].renewDate);
         }
     };
 })();

从Car类剥离的所有数据现在都保存在CarRecordManager这个单体的私用属性carRecordDatabase中。这个carRecordDatabase对象要比以前使用的一大批对象高效得多。那些处理所有权事宜的方法现在也被封装在这个单体中,因为他们处理的都是外在数据。可以看出,这种优化是以复杂为代价的。原先有的只是一个类,而现在却变成了一个类和两个单体对象。把一个对象的数据保存在两个不同的地方这种做法有点令人困惑,但与所解决的性能问题相比,这两点都只是小问题。如果运用得当,那么享元模式能够显著的提升程序的性能。

管理享元对象的外在数据有许多不同方法。使用管理器对象是一种常见做法,这种对象有一个集中管理的数据库(centralized database),用于存放外在状态及其所属的享元对象。汽车登记示例就采用了这种方案。其优点在于简单,容易维护。这也是一种比较轻便的方案,因为用来保存外在数据的只是一个数组或对象字面量。

建造者模式

工厂模式可有效的创建可复用的实例对象,关心的是最终创建的对象是什么,不关心创建的过程,因此通过工厂模式得到的都是对象实例或者类簇。建造者模式相对比工厂模式复杂一些,关心的是创建对象的过程,例如之前的工厂模式我们关心的创建一个Person类,创建它的共同基本信息,例如name和age以及job等,但是建造者模式不仅要关注Person类的创建过程,还要关注这个Person更多细节,比如穿什么衣服,兴趣爱好是什么,在创建Person类实例对象的基础上,还可以自由组合其他更多信息。

比如要创建一个人的简历模板,让其可以自由选择展示的信息

// Person类
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.getName = function() {
  return this.name
}

Person.prototype.getAge = function() {
  return this.age
}

// Job类
function Job(job) {
  switch(job) {
    case 'teacher':
      this.job = '教师'
      this.jobDesc = '数学教师'
      break
    
    case 'doctor':
      this.job = '医生'
      this.jobDesc = '骨科医生'
      break
    
    case 'coder':
      this.job = '程序员'
      this.jobDesc = 'Web前端程序员'
      break

    default:
      this.job = job
      this.jobDesc = '不清楚您的职位的相关描述'
      break
  }
}


Job.prototype.changeJobDesc = function(desc) {
  this.jobDesc = desc
} 

Job.prototype.changeJob = function(job) {
  return this.job = job
}

// Hobby类
function Hobby(hobby) {
  this.hobby = []
  this.hobby.push(hobby)
}

Hobby.prototype.addHobby = function(hobby) {
  this.hobby.concat(hobby)
}

Hobby.prototype.getHobby = function() {
  return this.hobby.split(',')
}

// Skill类
function Skill(skill) {
  this.skill = []
  this.skill.push(skill)
}

// ...

// 简历类
function Resume(name, age) {
  this.person = new Person(name, age)
  this.person.job = new Job()
  this.person.hobby = new Hobby()
  this.person.skill = new Skill()
}

// 创建简历
let resume = new Resume('ziyi2', 28)
console.log(resume)

/*
person : Person {
  age : 28
  hobby: Hobby {
    hobby: Array(1)
  }
  job: Job {
    job: undefined, 
    jobDesc: "不清楚您的职位的相关描述
  }
  name:"ziyi2"
  skill: Skill {
    skill: Array(1)
  }
}  
*/

创建的对象更复杂,是一个复合对象。

适配器模式

将一个类(对象)的接口(方法或属性)转化成另外一个接口,从而满足用户的需求。

例如将之前的外观模式适配jQuery库

var Browser = {
  event: {
    add: function(dom, type, fn) {
      $(dom).on(type, fn)
    },

    remove: function(dom, type, fn) {
      $(dom).off(type, fn)
    }

    // ....
  },

  id: function(dom, id) {
    return $(id).get(0)
  }
}

需要注意适配的时候为了做到兼容参数此时不能变。

除了代码适配,还可以进行参数适配。当一个函数传入参数较多时,很难记住参数的顺序,因此可以通过传入对象来进行参数适配

function fn(name, age, job, desc) {}

function adapter(obj) {
  var _adapter = {
    name: '',
    age: 0,
    job: 'web',
    desc: ''
  }

  // 适配传入的参数
  for(var key in _adapter) {
    _adapter[key] = obj[key] || _adapter[key]
  }
}

MV*

MVC

MVC(模型-视图-控制器)是一种架构设计模式,通过关注点分离鼓励改进引用程序组织。它强制将业务数据(Model)、用户界面(View)隔离,并将控制器(Controller)用于管理逻辑和用户输入。在JavaScript中的MVC框架包括Backbone、Ember.js和AngularJS。

MVC

MVC有利于简化应用程序功能的模块化。

  • 整体维护更容易。
  • 解耦Model和View。
  • 消除Model和Controlle的代码重复。

Model

代表特定的数据,当Model改变时,会通过观察者模式发布信息。一般要求数据可持久化,保存在内存中、用户的localStorage数据存储中或者数据库中。Model主要和业务数据相关。

View

描绘Model当前状态,会通过观察者模式订阅Model更新或修改的信息,从而做出View的更新。一个View通常订阅一个Model,用户与View进行交互,包括读取和编辑Model。

Controller

处理用户交互,为View做决定,是Model和View的中介。当用户操作View时,通常用于更新Model。

优点

  • MVC有利于简化应用程序功能的模块化。
  • 解耦Model和View。
  • 消除Model和Controlle的代码重复。

缺点

  • Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,Controller业务逻辑的正确性是无法验证的:Controller更新Model的时候,无法对View的更新操作进行断言。
  • View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的

MVC Model 2

在Web服务端开发的时候也会接触到MVC模式,而这种MVC模式不能严格称为MVC模式。经典的MVC模式只是解决客户端图形界面应用程序的问题,而对服务端无效。服务端的MVC模式又自己特定的名字:MVC Model 2,或者叫JSP Model 2,或者直接就是Model 2 。Model 2客户端服务端的交互模式如下:

MVC Model2

服务端接收到来自客户端的请求,服务端通过路由规则把这个请求交由给特定的Controller进行处理,Controller执行相应的业务逻辑,对数据库数据(Model)进行操作,然后用数据去渲染特定的模版,返回给客户端。

因为HTTP协议是单工协议并且是无状态的,服务器无法直接给客户端推送数据。除非客户端再次发起请求,否则服务器端的Model的变更就无法告知客户端。所以可以看到经典的Smalltalk-80 MVC中Model通过观察者模式告知View更新这一环被无情地打破,不能称为严格的MVC。

Model 2模式最早在1998年应用在JSP应用程序当中,JSP Model 1应用管理的混乱诱发了JSP参考了客户端MVC模式,催生了Model 2。

MVC Model

后来这种模式几乎被应用在所有语言的Web开发框架当中。PHP的ThinkPHP,Python的Dijango、Flask,NodeJS的Express,Ruby的RoR,基本都采纳了这种模式。平常所讲的MVC基本是这种服务端的MVC。

MVP

MVP(模型-视图-表示器)是MVC设计模式的一种衍生模式,专注于改进表示逻辑。MVP打破了View原来对于Model的依赖,其余的依赖关系和MVC模式一致。

MVP

和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter同样的会执行相应的业务逻辑,并且对Model进行相应的操作;而这时候Model也是通过观察者模式把自己变更的消息传递出去,但是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面。

对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。

优点

  • 便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter业务逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例。

  • View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务逻辑完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做高度可复用的View组件。

缺点

  • Presenter中除了业务逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。

MVVM

MVVM可以看作是一种特殊的MVP(Passive View)模式,或者说是对MVP模式的一种改良。

MVVM代表的是Model-View-ViewModel,这里需要解释一下什么是ViewModel。ViewModel的含义就是 "Model of View",视图的模型。它的含义包含了领域模型(Domain Model)和视图的状态(State)。 在图形界面应用程序当中,界面所提供的信息可能不仅仅包含应用程序的领域模型。还可能包含一些领域模型不包含的视图状态,例如电子表格程序上需要显示当前排序的状态是顺序的还是逆序的,而这是Domain Model所不包含的,但也是需要显示的信息。

可以简单把ViewModel理解为页面上所显示内容的数据抽象,和Domain Model不一样,ViewModel更适合用来描述View。

MVVM

MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。以前全部由Presenter负责的View和Model之间数据同步操作交由给Binder处理。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。

也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。

优点

  • 提高可维护性。解决了MVP大量的手动View和Model同步的问题,提供双向绑定机制。提高了代码的可维护性。
  • 简化测试。因为同步逻辑是交由Binder做的,View跟着Model同时变更,所以只需要保证Model的正确性,View就正确。大大减少了对View同步更新的测试。

缺点

  • 过于简单的图形界面不适用,或说牛刀杀鸡。
  • 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。
  • 数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。