JavaScript面向对象与设计模式

1. 面向对象

1.1 封装

封装的目的在于将信息隐藏。广义的封装不仅包括封装数据封装实现,还包括封装类型封装变化

1.1.1 封装数据

  1. Java中,封装数据是由语法解析来实现的,提供了publicprotectedprivate等关键字来提供不同的访问权限。
  2. JavaScript中,只能依赖变量作用域来模拟实现封装性。
var myObj = (function() {
  var _name = 'sven'
  return {
    getName: function() {
      return _name
    }
  }
})()

console.log(myObj.getName()) // sven
console.log(myObj._name) // undefined

1.1.2 封装实现

  1. 封装实现细节使对象内部的变化对其他对象而言是透明的,即不可见的。
  2. 封装实现细节使对象之间的耦合变松散,对象之间只通过暴露的API接口来通讯。当我们修改一个对象时,可以随意修改它的内部实现。
  3. 封装实现细节的例子有很多。例如:迭代器each函数。

1.1.3 封装类型

  1. 对于静态类型语言,封装类型是通过抽象类和接口来进行的。将对象的真正类型隐藏在抽象类或者接口之后。
  2. JavaScript是一门类型模糊语言。在封装类型方面,JavaScript没有能力,也没有必要做得更多。
  3. 对于JavaScript设计模式实现来说,不区分类型是一种失色,也可以说是一种解脱。

1.1.4 封装变化

  1. 考虑你的设计中哪些地方可能变化,找到并封装,这是许多设计模式的主题。
  2. 设计模式被划分为创建型模式、结构型模式以及行为型模式。其中,创建型模式的目的就是封装创建对象的变化,结构型模式封装的是对象之间的组合关系,行为型模式封装的是对象的行为变化
  3. 通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大限度的保证程序的稳定性和可扩展性。

1.2 继承

  1. JavaScript选择了基于原型的面向对象系统。在原型编程的思想中,类并不是必须的,对象未必从类中创建而来,一个对象可以通过克隆另一个对象而得到。
  2. 虽然JavaScript的对象最初都是由Object.prototype对象克隆而来的,但对象构造器的原型可以动态指向其它对象。这样一来,当对象a需要借用对象b的能力时,可以有选择性地把对象a的构造器的原型指向对象b,从来达到继承的效果。
  3. 原型继承
var obj = {name: 'sven'}
var A = function() {}

A.prototype = obj

var a = new A()
console.log(a.name) //sven

name属性查找:a → a.__proto__→ obj

  1. 原型继承链
var obj = {name: 'sven'}

var A = function() {}
A.prototype = obj

var B = function() {}
B.prototype = new A()

var b = new B()
console.log(b.name) //sven

name属性查找链:b → b.__proto__ → new A() → A.prototype → obj

1.3 多态

  1. 多态将“做什么”和“谁去做”分离开来。实现多态的关键在于消除类型之间的耦合关系。
  2. Java中,可以通过向上转型来实现多态。由于JavaScript的变量类型在运行期是可变的,所以JavaScript对象的多态性是与生俱来的。
  3. 多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。
  4. 多态将过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
  5. 代码演示
class GoogleMap {
  show() {
    console.log('开始渲染谷歌地图')
  }
}

class BaiduMap {
  show() {
    console.log('开始渲染百度地图')
  }
}

const renderMap = map => {
  if(map.show instanceof Function) {
    map.show()
  }
}

renderMap(new GoogleMap())
renderMap(new BaiduMap())

1.4 UML类图

  1. 类图


    类图
  2. 类与类之间的关系
    (1) 泛化表示继承,用空心箭头表示。
    (2) 关联表示引用,用实心箭头表示。


    类与类关系图

1.5 示例

  1. 使用class简单实现jQuery中的$选择器
class jQuery {
  constructor(selector) {
    const slice = Array.prototype.slice
    const dom = slice.call(document.querySelectorAll(selector))
    this.selector = selector || ''
    const len = dom ? dom.length : 0;
    this.length = len
    for(let i = 0; i < len; i++) {
      this[i] = dom[i]
    }
  }
  append(node) {}
  addClass(name) {}
  html(data) {}
}

window.$ = selector => new jQuery(selector)
  1. 打车时可以打专车或快车。任何车都有车牌号和名称。快车每公里1元,专车每公里2元。行程开始时,显示车辆信息。行程结束时,显示打车金额。行程距离为5公里。


    UML类图
//父类 - 车
class Car {
  constructor(name, number) {
    this.name = name
    this.number = number
  }
}

//子类 - 快车
class KuaiChe extends Car {
  constructor(name, number) {
    super(name, number)
    this.price = 1
  }
}

//子类 - 专车
class ZhuanChe extends Car {
  constructor(name, number) {
    super(name, number)
    this.price = 2
  }
}

//行程
class Trip {
  constructor(car, distance) {
    this.car = car
    this.distance = distance
  }

  start() {
    console.log(`行程开始 车名为${this.car.name},车牌号为${this.car.number}`)
  }

  end() {
    console.log(`行程结束 车费为${this.distance * this.car.price}`)
  }
}

const zhuanChe = new ZhuanChe('专车', '299567')
const trip = new Trip(zhuanChe, 5)
trip.start()
trip.end()

注意:将行程抽象为一个类,而不是车的一个属性。

  1. 某停车厂分3层,每层100个车位。每个车位都能够检测到车辆的驶入和离开。车辆进入前,显示每层空余车位数量。车辆进入时,摄像头可识别车牌号和时间。车辆出来时,出口显示器显示车牌号和停车时间。
    类: 停车场、层、车位、车辆、摄像头、显示器。


    UML类图
//车辆
class Car {
  constructor(number) {
    this.number = number
  }
}

//停车位
class Stall {
  constructor() {
    this.empty = true
  }

  in() {
    this.empty = false
  }

  out() {
    this.empty = true
  }
}

//停车层
class Floor {
  constructor(index, stalls) {
    this.index = index
    this.stalls = stalls || []
  }

  emptyNum() {
    return this.stalls.filter(stall => stall.empty).length
  }
}

//出口显示屏
class Screen {
  show(car, inTime) {
    console.log(`车牌号为${car.number},停留时间为${Date.now() - inTime}`)
  }
} 

//入口摄像头
class Camera {
  shoot(car) {
    return {
      number: car.number,
      inTime: Date.now()
    }
  }
}

//停车场
class Park {
  constructor(floors, camera, screen) {
    this.camera = camera
    this.screen = screen
    this.floors = floors || []
    this.carList = {};
  }

  emptyNum() {
    let num = 0
    this.floors.forEach(floor => {
      const emptyNum = floor.emptyNum()
      num += emptyNum
    })
    return num;
  }

  showMsg() {
    let info = ''
    for(let i = 1; i < this.floors.length; i++) {
      const floor = this.floors[i]
      info += `第${floor.index}层还有${floor.emptyNum()}个空位`
    }
    console.log(info)
  }

  in(car) {
    if(this.emptyNum() > 0) {
      const info = this.camera.shoot(car)
      for(let i = 1; i < this.floors.length; i ++) {
        const floor = this.floors[i]
        const allNum = floor.stalls.length
        const emptyNum = floor.emptyNum()
        if(emptyNum > 0) {
          let index = 1; 
          while(!floor.stalls[index].empty) {
            index++
          }
          const stall = floor.stalls[index]
          stall.in()
          //保存停车位信息
          info.stall = stall
          break
        }
      } 
      this.carList[car.number] = info
    } else {
      console.log('停车场已满')
    }
  }

  out(car) {
    const info = this.carList[car.number]
    info.stall.out()
    this.screen.show(car, info.inTime)
    delete this.carList[car.number]
  }
}

//测试代码
const floors = []
for(let i = 1; i <= 3; i ++) {
  const stalls = []
  for(let j = 1; j <= 100; j++) {
    stalls[j] = new Stall()
  }
  floors[i] = new Floor(i, stalls)
}

const camera = new Camera()
const screen = new Screen()
const park = new Park(floors, camera, screen)
const car1 = new Car('100')
const car2 = new Car('200')
const car3 = new Car('300')
park.in(car1)
park.showMsg()
park.in(car2)
park.showMsg()
park.out(car1)
park.showMsg()
park.in(car3)
park.showMsg()
park.out(car2)
park.showMsg()
park.out(car3)
park.showMsg()

注意:① 引用指的是一个类持有另一个类,而不是一个类的方法以另一个类为参数。 ② 类与类之间应该尽量减少耦合,能够通过将另一个类作为参数实现,就不要持有另一个类。

2. 设计模式

  1. 简介
    在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。即设计模式是在某种场合下对某个问题的一种解决方案。
  2. 分类
    (1) 创建型
    工厂模式、单例模式、原型模式。
    (2) 结构型
    适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
    (3) 行为型
    策略模式、模板方法模式、观察者模式、迭代器模式、职责链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

2.1 工厂模式

  1. 介绍
    new创建对象的封装。
  2. UML类图
    UML类图
  3. 使用场景

(1) jQuery中的$('div')

class jQuery{
   // ...
}
window.$ = function(selector) {
   return new jQuery(selector)
}

(2) React中的React.createElement

class Vnode(tag, attrs, children) {
   // ...
}
React.createElement = function(tag, attrs, children) {
    return new Vnode(tag, attrs, children)
}

(3) Vue异步组件

Vue.component('async-example', function(resolve, reject) {
  setTimeout(function() {
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

工厂方式创建对象与new创建对象相比,书写简便并且封装性更好。

2.2 单例模式

  1. 介绍
    保证一个类仅有一个实例,并提供一个访问它的全局访问点。
    注释:单例模式的核心是确保只有一个实例,并提供全局访问。
  2. 代码演示
class Single {
  login() {
    console.log('login')
  }
}

Single.getInstance = (function() {
  let instance
  return () => {
    if(!instance) {
      instance = new Single()
    }
    return instance
  }
})()

let obj1 = Single.getInstance()
obj1.login()
let obj2 = Single.getInstance()
obj1.login()

console.log(obj1 === obj2) //true

注释:① Single类的使用者必须知道这是一个单例类,与以往通过new Single()的方式不同,这里必须使用Single.getInstance()来创建单例对象。② 该示例为惰性单例,在使用的时候才创建对象。

  1. JavaScript中的单例模式
    在传统面向对象语言中,单例对象从类中创建而来。JavaScript其实是一门无类语言,创建唯一对象并不需要先创建一个类。传统的单例模式实现在JavaScript中并不适用。
const single = {
  a() {
    console.log('1')
  }
}
  1. 创建对象与单例模式分离
const getSingle = fn => {
  let result
  return () => result || (result = fn(arguments)) 
}

const createObj = () => new Object();

const createSingleObj = getSingle(createObj)
const s1 = createSingleObj()
const s2 = createSingleObj()
console.log(s1) //{}
console.log(s1 === s2) //true

注释:创建对象与管理单例的职责被分布在两个不同的方法中。

  1. 使用场景

(1) jQuery中只有一个$

if(window.jQuery) {
  return window.jQuery
} else {
  // 初始化...
}

(2) vuexredux中的store

2.3 适配器模式

  1. 介绍
    旧接口格式和使用者不兼容,使用适配器转换接口。
  2. UML类图
    image.png
  3. 代码演示
class Adaptee {
  specifiRequest() {
    return '德国标准插头'
  }
}

class Target {
  constructor() {
    this.adaptee = new Adaptee()
  }

  request() {
    let info = this.adaptee.specifiRequest()
    return `${info} - 转换器 - 中国标准插头`
  }
 }

 let target = new Target()
 console.log(target.request()) 
  1. 使用场景

(1) 封装旧接口

//适配jQuery中的$.ajax()
const $ = {
  ajax: function(options) {
    return ajax(options)
  }
}

(2) vue computed

  <div id="app">
    <p>顺序: {{message}}</p>
    <p>逆序: {{reverseMessage}}</p>
  </div>
  <script src = "https://cdn.bootcss.com/vue/2.5.14/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        message: 'hello'
      },
      computed: {
          reverseMessage: function() {
            return this.message.split('').reverse().join('')
          }
        }
    })
  </script>

2.4 装饰器模式

  1. 介绍
    装饰器模式可以动态地给某个对象添加额外的职责,而不会影响从这个类中派生的其他对象。
  2. 模式分析
    在传统的面向对象语言中,给对象添加功能常常使用继承的方式。继承的方式并不灵活,会导致超类和子类存在强耦合性,并且在完成一些功能复用的同时,有可能创建出大量的子类。
    装饰器模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。与继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。
  3. UML类图
    image.png
  4. 代码演示
class Plane{
  fire() {
    console.log('发射普通子弹')
  }
}

class MissileDecorator {
  constructor(plane) {
    this.plane = plane
  }

  fire() {
    this.plane.fire()
    console.log('发射导弹')
  }
}

class AtomDecorator {
  constructor(plane) {
    this.plane = plane
  }

  fire() {
    this.plane.fire()
    console.log('发射原子弹')
  }
}

let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire() //发射普通子弹 发射导弹 发射原子弹
  1. beforeafter钩子函数
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 fun = function() {
  console.log(1)
 }

 var beforeFun = fun.before(function() {
   console.log(0)
 })
 beforeFun() //0 1 


 var afterFun = fun.after(function() {
   console.log(2)
 }).after(function() {
   console.log(3)
 })
 afterFun() //1 2 3
  1. ES7中的装饰器
    (1) 配置环境
    ① 安装插件
    ➜ design npm i babel-plugin-transform-decorators-legacy --save-dev
    ② 修改webpack打包配置文件
module.exports = {
 //...
  module: {
    rules: [{
    test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['babel-preset-env'],
          plugins: ['babel-plugin-transform-decorators-legacy']
        }
      }
    }]
  },
 //...
}

(2) 装饰类

@testDec1
@testDec2(false)
class Demo {}

function testDec1( target) {
  target.isDec1 = true
}

function testDec2( bool ) {
  return function(target) {
    target.isDec2 = bool
  }
}

console.log(Demo.isDec1) //true
console.log(Demo.isDec2) //false

(3) 装饰对象

function mixins(...list) {
  return function(target) {
    Object.assign(target.prototype, ...list)
  }
}

const Foo = {
  foo() {
    console.log('foo')
  }
}

@mixins(Foo)
class B {}

const b = new B()
b.foo() //foo

(4) 装饰方法
① 设置方法只可执行,不可修改

class Person {
  constructor() {
    this.first = 'A'
    this.last = 'B'
  }

  @readonly
  name() { return `${this.first} ${this.last}` }
}

function readonly(target, name, descriptor){
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

const p = new Person()
//A B
console.log(p.name())  
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'
p.name = () => {} 

② 方法执行添加日志

class Math{
  @log
  add(a, b) {
    return a + b
  }
}

function log(target, name, descriptor) {
  let oldValue = descriptor.value
  descriptor.value = function() {
    console.log(`calling ${name} wich`, arguments)
    return oldValue.apply(this, arguments)
  }
  return descriptor
}

const math = new Math()
const result = math.add(2, 4)
console.log(result)
  1. core-decorators 第三方库
    提供常用的装饰器。
import { readonly } from 'core-decorators'
class Person {
  constructor() {
    this.first = 'A'
    this.last = 'B'
  }

  @readonly
  name() { 
     return `${this.first} ${this.last}` 
  }

  @deprecate()
  getName() {
    return `${this.first} ${this.last}`
  }
}

const p = new Person()
console.log(p.name()) 
//A B 
console.log(p.getName()) 
//DEPRECATION Person#getName: This function will be removed in future versions.
//A B 
p.name = () => {} 
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'

2.5 代理模式

  1. 介绍
    使用者无权访问目标对象,通过代理做授权和控制。
  2. UML类图
    UML类图
  3. 代码演示
class RealImg {
  constructor(fileName) {
    this.fileName = fileName
    this.loadFromDisk()
  }

  display() {
    console.log(`display... ${this.fileName}`)
  }

  loadFromDisk() {
    console.log(`loading... ${this.fileName}`)
  }
}

class ProxyImg {
  constructor(fileName) {
    this.realImg = new RealImg(fileName)
  }

  display() {
    this.realImg.display()
  }
}

const proxyImg = new ProxyImg('1.png')
proxyImg.display()

注释:代理和本体接口必须一致,在任何使用本体的地方都可以替换成使用代理。

  1. 使用场景

(1) 事件代理

var ul = document.getElementById('ul')
ul.addEventListener('click', function(e) {
  var target = e.target;
  if(target.nodeName === 'LI') {
    alert(target.innerHTML)
  }
})

(2) jQuery中的$.proxy

$('ul').click(function() {
  var self = this
  setTimeout(function() {
     $(self).css('background', 'red')
  }, 1000) 
})

//使用$.proxy保存this
$('ul').click(function() {
  setTimeout($.proxy(function() {
     $(this).css('background', 'red')
  }, this), 1000) 
})

(3) 图片预加载

var myImg = (function() {
  var imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return function(src){
    imgNode.src = src
  }
})()

var proxyImg = (function() {
  var img = new Image()
  img.onload = function() {
    myImg(this.src)
  }

  return function(src) {
    myImg('loading.gif')
    img.src = src
  }
})()

proxyImg('1.png')

(4) 缓存代理

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

//缓存代理工厂
const createProxyFactory = fn => {
  let cache = {}
  return function(){
    const args = Array.prototype.join.call(arguments, ',')
    if(cache[args]) {
      return cache[args]
    }
    return cache[args] = fn.apply(this, arguments)
  }
}

var proxyMult = createProxyFactory(mult)
console.log(proxyMult(1,2,3,4)) //24
console.log(proxyMult(1,2,3,4)) //24

(5) ES6中的Proxy

//明星
let star = {
  name: '张XX',
  age: 25,
  phone: '13900001111'
}

//经纪人
let agent = new Proxy(star, {
  get: function (target, key, receiver) {
    if(key === 'phone') {
      //返回经纪人自己的电话
      return '13922225555'
    }
    if(key === 'price') {
      //明星不报价,经纪人报价
      return 120000
    }
    return target[key]
  },
  set: function (target, key, value, receiver) {
    if(key === 'customPrice') {
      if(value < 100000) {
        throw new Error('价格太低')
      } else {
        target[key] = value
        return true
      }
    }
  }
})

console.log(agent.name) //张XX
console.log(agent.age) //25 
console.log(agent.phone) //13922225555
console.log(agent.price) //120000
agent.customPrice = 150000
console.log(star.customPrice) //150000
agent.customPrice = 90000 //Uncaught Error: 价格太低
  1. 与其他设计模式对比
    (1) 代理模式与适配器模式对比
    适配器模式:提供一个不同的接口。
    代理模式:提供一模一样的接口。
    (2) 代理模式与装饰器模式
    装饰器模式:扩展功能,原有功能不变且可直接使用。
    代理模式:使用原有功能,但是经过限制和约束。

2.6 外观模式

  1. 介绍
    为子系统中的一组接口提供了一个高层接口,使用者使用这个高层接口。
  2. 使用场景
function bindEvent(elem, type, selector, fn) {
  if(fn == null) {
    fn = selector
    selector = null
  }
  //...
}

// 调用
bindEvent(elem, 'click', '#div1', fn)
bindEvent(elem, 'click', fn)

2.7 观察者模式

  1. 介绍
    观察者模式又叫做发布-订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都将得到通知。
  2. UML类图
    image.png
  3. 代码演示
class Subject {
  constructor() {
    this.state = 0
    this.observers = []
  }
  
  getState() {
    return this.state
  }

  setState(state) {
    this.state = state
    this.notifyAllObservers()
  }

  notifyAllObservers() {
    this.observers.forEach(observer => {
      observer.update(this.state)
    })
  }

  attach(observer) {
    this.observers.push(observer)
    observer.subject = this
  }
}

class Observer {
  constructor(name) {
    this.name = name
    this.subject = null
  }

  update(state) {
    console.log(`${this.name} update state to ${state}`)
  }
}

const sub = new Subject()
const obs1 = new Observer('obs1')
const obs2 = new Observer('obs2')
sub.attach(obs1)
sub.attach(obs2)

sub.setState(1)
sub.setState(2)
sub.setState(3)
  1. 使用场景

(1) DOM事件

document.body.addEventListener('click', function() {
  console.log(1)
}, false)

document.body.addEventListener('click', function() {
  console.log(2)
}, false)

document.body.click() //模拟用户点击

(2) Promise

(3) jQuerycallbacks

var callbacks = $.Callbacks()
callbacks.add(function(info) {
  console.log('fn1', info)
})
callbacks.add(function(info) {
  console.log('fn2', info)
})
callbacks.add(function(info) {
  console.log('fn3', info)
})

callbacks.fire('go')
callbacks.fire('fire')

(4) nodejs自定义事件
EventEmitter

const EventEmitter = require('events').EventEmitter

const emitter = new EventEmitter()
emitter.on('some', info => {
  console.log('fn1', info)
})

emitter.on('some', info => {
  console.log('fn2', info)
})
emitter.emit('some', 'fire1')

EventEmitter应用

const EventEmitter = require('events').EventEmitter

class Dog extends EventEmitter {
  constructor(name) {
    super()
    this.name = name
  }
}

const simon = new Dog('simon')
simon.on('bark', function(voice) {
  console.log(this.name, voice)
})
setInterval(function() {
  simon.emit('bark', '汪')
}, 1000)

fs文件系统读取文件字符数

const fs = require('fs')
const readStream = fs.createReadStream('./node_modules/accepts/index.js')
let length = 0
readStream.on('data', function(chunk) {
  let len = chunk.toString().length
  console.log('len', len) //len 5252
  length += len
})
readStream.on('end', function() {
  console.log('length', length) //length 5252
})

fs文件系统读取文件行数

const fs = require('fs')
const readLine = require('readline')
let rl = readLine.createInterface({
  input: fs.createReadStream('./node_modules/accepts/index.js'),
})
let lineNum = 0
rl.on('line', function(line) {
  lineNum ++
})
rl.on('close', function() {
  console.log('lineNum', lineNum) //lineNum 238
})

2.8 迭代器模式

  1. 介绍
    提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
    注释:object不是有序数据集合,ES6中的map是有序数据集合。
  2. UML类图
    UML类图
  3. 代码演示
class Iterator {
  constructor(container) {
    this.list = container.list
    this.index = 0
  }

  next() {
    if(this.hasNext()) {
      return this.list[this.index++] 
    }
    return null
  }

  hasNext() {
    return this.index < this.list.length
  }
}

class Container {
  constructor(list) {
    this.list = list
  }

  getIterator() {
    return new Iterator(this)
  }
}

var arr = [1, 2, 3, 4, 5]
var container = new Container(arr)
var iterator = container.getIterator()
while(iterator.hasNext()) {
  console.log(iterator.next())
}
  1. 使用场景

(1) jQuery中的each方法

var arr = [1,2,3]
var nodeList = document.getElementsByTagName('li')
var $a = $('li')

function each(data) {
  var $data = $(data)
  $data.each(function(key, val) {
    console.log(key, val)
  })
}

each(arr)
each(nodeList)
each($a)

(2) 迭代器模式替换if...else

const getActiveUploadObj = () => {
  try{
    return new ActiveXObject('TXFTNActiveX.FTNUpload')
  } catch (e) {
    return false
  }
}

const getFlashUploadObj = () => {
  if(supportFlash()) {
    const str = '<object type="application/x-shockwave-flash"></object>'
    return $(str).appendTo($('body'))
  }
}

const getFormUploadObj = () => {
  const str = '<input name="file" type="file" class="ui-file" />'
  return $(str).appendTo($('body'))
}

const interatorUploadObj = function(){
  for(let i = 0, fn; fn=arguments[i++];) {
    var uploadObj = fn()
    if(uploadObj !== false) {
      return uploadObj
    }
  } 
}

const uploadObj = interatorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)

(3) ES6中的Iterator
ES6语法中,有序数据集合的数据类型有多个。例如:ArrayMapSetStringTypedArray等。需要有一个统一的遍历接口来遍历所有数据类型。以上数据类型都有一个[Symbol.iterator]属性。属性值是函数,执行函数返回一个迭代器。此迭代器有next()方法可顺序迭代子元素。

Array.prototype[Symbol.iterator]
ƒ values() { [native code] }
Array.prototype[Symbol.iterator]()
Array Iterator {}
Array.prototype[Symbol.iterator]().next()
{value: undefined, done: true}

Iterator使用示例

function each(data) {
  let iterator = data[Symbol.iterator]()

  let item = { done: false }
  while(!item.done) {
    item = iterator.next()
    if(!item.done) {
      console.log(item.value)
    }
  }
}

var map = new Map()
map.set(1, 'a')
map.set(2, 'b')
var arr = [1,2,3]
var str = 'abc'
each(arr)
each(str)
each(map)

ES6中的for...of语法是对Iterator的封装

function each(data) {
  for(let item of data) {
    console.log(item)
  }
}

var map = new Map()
map.set(1, 'a')
map.set(2, 'b')
var arr = [1,2,3]
var str = 'abc'
each(arr)
each(str)
each(map)

2.9 状态模式

  1. 介绍
    将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。
    注释:状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
  2. UML类图
    image.png
  3. 代码演示
class OffLightState {
  constructor(light) {
    this.light = light
  }

  switch() {
    console.log('微光') //offLightState对应的行为
    this.light.setState(this.light.weakLightState) //切换状态到weakLightState
  }
}

class WeakLightState {
  constructor(light) {
    this.light = light
  }

  switch() {
    console.log('强光')
    this.light.setState(this.light.strongLightState)
  }
}

class StrongLightState {
  constructor(light) {
    this.light = light
  }

  switch() {
    console.log('关灯')
    this.light.setState(this.light.offLightState)
  }
}

class Light {
  constructor() {
    this.offLightState = new OffLightState(this)
    this.weakLightState = new WeakLightState(this)
    this.strongLightState = new StrongLightState(this)
  }

  init() {
    this.currState = this.offLightState
  }

  setState(newState) {
    this.currState = newState
  }

  press() {
    this.currState.switch()
  }
}

const light = new Light()
light.init()
light.press() //微光
light.press() //强光
light.press() //关灯

注释:状态的切换规律事先被完好定义在各个状态类中。在Context中再也找不到任何一个跟状态切换相关的条件分支语句。

  1. 使用场景
    (1) 有限状态机
    有限个状态知之间切换。
    使用开源库:javascript-state-machine
import StateMachine from 'javascript-state-machine'
import $ from 'jquery'

let fsm = new StateMachine({
  init: '收藏',
  transitions: [
    {
      name: 'doStore',
      form: '收藏',
      to: '取消收藏'
    },
    {
      name: 'deleteStore',
      form: '取消收藏',
      to: '收藏'
    }
  ],
  methods: {
    onDoStore: function() {
      alert('收藏成功') 
      updateText()
    },
    onDeleteStore: function() {
      alert('取消收藏成功') 
      updateText()      
    }
  }
})

let $btn = $('#btn')

$btn.click(function() {
  if(fsm.is('收藏')) {
    fsm.doStore()
  } else {
    fsm.deleteStore()
  }
})

// 更新按钮文案 
function updateText() {
  $btn.text(fsm.state)
}

//初始化文案
updateText()

(2) Promise实现

import StateMachine from 'javascript-state-machine'

const fsm = new StateMachine({
  init: 'pending',
  transitions: [
    {
      name: 'resolve',
      from: 'pending',
      to: 'fullfilled'
    },
    {
      name: 'reject',
      from: 'pending',
      to: 'rejected'
    }
  ],
  methods: {
    onResolve(state, data){
      //state - 当前状态机实例; data - fsm.resolve(xxx) 传递的参数
      data.successList.forEach(fn => fn())
    },
    onReject(state, data){
      //state - 当前状态机实例; data - fsm.reject(xxx) 传递的参数
      data.failList.forEach(fn => fn())
    }
  }
})

class MyPromise {
  constructor(fn) {
    this.successList = []
    this.failList = []
    fn(() => {
      // resolve 函数
      fsm.resolve(this)
    }, () => {
      // reject 函数
      fsm.reject(this)
    })
  }

  then(successFn, failFn) {
    this.successList.push(successFn)
    this.failList.push(failFn)
  }
}

function loadImg(src) {
  const mp = new MyPromise((resolve, reject) => {
    const img = document.createElement('img')
    img.onload = () => resolve(img)
    img.onerror = () => reject()
    img.src = src
  })
  return mp
}

let src = 'https://www.baidu.com/img/bd_logo1.png'
let mp = loadImg(src)

mp.then(() => {
  console.log('success1')
}, () => {
  console.log('fail1')
})

mp.then(() => {
  console.log('success2')
}, () => {
  console.log('fail2')
})
  1. 与策略模式对比
    策略模式中各个策略类之间是平等又平行的,他们之间没有任何联系,客户必须熟知这些策略类的所用,以便随时主动切换算法;而在状态模式中,状态之间的切换早已被预先设定,“改变行为”这件事情发生在状态模式内部,客户并不需要了解这些细节。

2.10 原型模式

JavaScript选择了基于原型的面向对象系统。在原型编程的思想中,类并不是必须的,对象未必从类中创建而来,一个对象可以通过克隆另一个对象而得到。

2.10.1 使用克隆的原型模式

  1. 原型模式是用于创建对象的一种模式。我们不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象
  2. ES5中提供了Object.create方法,可以用来克隆对象。
let obj = {
  getName() {
    return this.first + '  ' + this.last
  },

  say() {
    alert('hello')
  }
}

let x = Object.create(obj)
x.first = 'A'
x.last = 'B'
alert(x.getName())
x.say()

在不支持Object.create方法的浏览器中,则可以使用以下代码:

//兼容不支持浏览器
Object.create = Object.create || function(obj) {
  var F = function() {}
  F.prototype = obj
  return new F()
}
  1. 原型模式提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。通过克隆,我们不再关心对象的具体类型。

2.10.2 JavaScript中的原型继承

  1. 原型编程遵循以下基本规则
    ① 所有的数据都是对象。
    ② 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
    ③ 对象会记住它的原型。
    ④ 如果对象无法响应某个请求,它会把这个请求委托给自己的原型。
  2. JavaScript中的根对象是Object.prototypeObject.prototype对象是一个空的对象。JavaScript中的每一个对象,实际上都是从Object.prototype对象克隆而来的。
var obj1 = new Object()
var obj2 = {}

console.log(Object.getPrototypeOf(obj1) === obj1.__proto__)  //true
console.log(Object.getPrototypeOf(obj1) === Object.prototype) //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype) //true

2.11 桥接模式

  1. 介绍
    把抽象化与实现化解耦,使两者可以独立变化。
  2. 代码演示
class Color {
  constructor(name) {
    this.name = name
  }
}

class Shape {
  constructor(name, color) {
    this.name = name
    this.color = color
  }

  draw() {
    console.log(`${this.color.name} ${this.name}`)
  }
}

let red = new Color('red')
let circle = new Shape('circle', red)
circle.draw() //red circle

2.12 组合模式

  1. 介绍
    生成树形结构,表示“整体-部分”关系。让整体和部分具有一致的操作方式。
    注释:组合模式最大的优点在于可以一致地对待组合对象和基本对象。客户不需要知道当前处理的是宏任务还是普通任务。
  2. 特点
    ① 使用树形方式创建对象的结构。
    ② 把相同操作应用在组合对象和单个对象上。
  3. 使用场景
    虚拟DOM中的vnode是这种形式。
  4. 组合模式注意事项
    ① 组合对象和叶对象是聚合关系而非父子关系。
    ② 组合对象和叶对象拥有相同的接口。
    ③ 对组合对象和叶对象的操作必须具有一致性。

2.13 享元模式

  1. 介绍
    享元模式是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
  2. 代码演示
class Model {
  constructor(sex) {
    this.sex = sex
    this.underwear = ''
  }

  setUnderwear(underwear) {
    this.underwear = underwear
  }

  takePhoto() {
    console.log(`sex: ${this.sex}; underwear: ${this.underwear}`)
  }
}

const maleModel = new Model('male')
const femaleModel = new Model('female')

for(let i = 1; i<= 50; i++) {
  maleModel.setUnderwear('underwear' + i)
  maleModel.takePhoto()
}

for(let j = 1; j<= 50; j++) {
  femaleModel.setUnderwear('underwear' + j)
  femaleModel.takePhoto()
}
  1. 内部状态与外部状态
    享元模式要求将对象的属性划分为内部状态和外部状态(状态在这里通常指属性)。
    ① 内部状态存储于对象内部。
    ② 内部状态可以被一些对象共享。
    ③ 内部状态独立于具体的场景,通常不会改变。
    ④ 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

2.14 策略模式

  1. 介绍
    定义一系列的算法,把不同策略封装起来,并且使他们可以相互替换。
    注释:策略模式可以替代if...else...语句。
  2. 代码演示
//不使用策略模式
class User {
  constructor(type) {
    this.type = type
  }

  buy() {
    if(this.type === 'oridinary') {
      console.log('普通用户购买')
    } else if(this.type === 'member') {
      console.log('会员用户购买')
    } else if('this.type' === 'vip') {
      console.log('vip 用户购买')
    }
  }
}

let u1 = new User('member') 
u1.buy() //会员用户购买

//使用策略模式
class OridinaryUser {
  buy() {
    console.log('普通用户购买')
  }
}
class MemberUser {
  buy() {
    console.log('会员用户购买')
  }
}

class VipUser {
  buy() {
    console.log('vip 用户购买')
  }
}

class UserManager {
  constructor() {
    this.user = null
  }

  setUser(user) {
    this.user= user
  }

  userBuy() {
    this.user.buy()
  }
}

const m = new UserManager()
const u = new OridinaryUser()
m.setUser(u)
m.userBuy() //普通用户购买

注释:策略模式的实现至少由两部分组成。第一个部分是一组策略类(OridinaryUserMemberUserVipUser),策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类(UserManager),环境类接受客户的请求,随后把请求委托给某一个策略类。

  1. JavaScript中的策略模式
    策略对象从各个策略类中创建而来,环境对象从环境类中创建而来,这是模拟一些传统面向对象语言的实现。在JavaScript中,函数也是对象,所以更简单和直接的做法是把策略对象和环境对象直接定义为函数。
const strategies = {
  oridinary() {
    console.log('普通用户购买')
  },
  member() {
    console.log('会员用户购买')
  },
  vip() {
    console.log('vip 用户购买')
  }
}

const userBuy = user => strategies[user]()
userBuy('member') //会员用户购买

2.15 模板方法模式

  1. 介绍
    在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
  2. 代码演示
class Beverage {
  init() {
    this.boilWater()
    this.brew()
    this.pourInCup()
    this.addCondiments()
  }

  boilWater() {
    console.log('把水煮沸')
  }

  brew(){} //沸水冲泡饮料
  pourInCup(){} //饮料倒进杯子
  addCondiments(){} //加调料
}

class Coffee extends Beverage {
  brew(){
    console.log('用沸水冲泡咖啡')
  } 
  pourInCup(){
    console.log('把咖啡倒进杯子')
  } 
  addCondiments(){
    console.log('加糖和牛奶')
  } 
}

class Tea extends Beverage {
  brew(){
    console.log('用沸水浸泡茶叶')
  } 
  pourInCup(){
    console.log('把茶倒进杯子')
  } 
  addCondiments(){
    console.log('加柠檬')
  } 
}

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

const tea = new Tea()
tea.init()
  1. 模式对比
    策略模式和模板方法是一对竞争者。在大多数情况下,他们可以相互替换使用。模板方法模式基于继承的思想,而策略模式则偏重于组合和委托。

2.16 职责链模式

  1. 介绍
    使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
  2. 代码演示
    ① 流程审批(职责链每个节点都处理)
class Action {
  constructor(name) {
    this.name = name
    this.nextAction = null
  }

  setNextAction(action) {
    this.nextAction = action
  }

  handle() {
    console.log(`${this.name} 审批`)
    this.nextAction && this.nextAction.handle()
  }
}

const action1 = new Action('组长')
const action2 = new Action('经理')
const action3 = new Action('总监')

action1.setNextAction(action2)
action2.setNextAction(action3)

action1.handle()

② 购买商品(职责链只有一个节点处理)

const order500 = (orderType, pay, stock) => {
  if(orderType === 1 && pay === true){
    console.log('500元定金预购,得到100优惠券')
    return true
  }
  return false
}

const order200 = (orderType, pay, stock) => {
  if(orderType === 2 && pay === true){
    console.log('200元定金预购,得到50优惠券')
    return true
  }
  return false
}

const orderNormal = (orderType, pay, stock) => {
  if(stock > 0){
    console.log('普通购买,无优惠券')
  } else {
    console.log('手机内存不足')
  }
  return true
}

class Chain {
  constructor(fn) {
    this.fn = fn
    this.successor = null
  }

  setNextSuccessor(successor){
    this.successor = successor
  }

  passRequest() {
    const res = this.fn.apply(this, arguments)
    return res ? res : this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
}

const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const chainOrderNormal = new Chain(orderNormal)

chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

chainOrder500.passRequest(1, true, 500) //500元定金预购,得到100优惠券
chainOrder500.passRequest(2, true, 500) //200元定金预购,得到50优惠券
chainOrder500.passRequest(3, false, 500) //普通购买,无优惠券
chainOrder500.passRequest(3, false, 0) //手机内存不足
  1. 使用场景
    ① 作用域链
    ② 原型链
    ③ 事件冒泡
    jQuery的链式操作
    Promise.then的链式操作

2.17 命令模式

  1. 介绍
    执行命令时,发布者和执行者分开。中间加入命令对象,作为中转站。
  2. 应用场景
    有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接受者能够消除彼此之间的耦合关系。
  3. 代码演示
//接受者
class Receiver{
  exec() {
    console.log('执行')
  }
}

//命令者
class  Command {
  constructor(receiver) {
    this.receiver = receiver
  }

  cmd() {
    console.log('执行命令')
    this.receiver.exec()
  }
}

//触发者
class Invoker {
  constructor(command) {
    this.command = command
  }

  invoke() {
    console.log('开始')
    this.command.cmd()
  }
}

//士兵
const soldier = new Receiver()
//小号手
const trumpeter = new Command(soldier)
//将军
const general = new Invoker(trumpeter)

general.invoke()
  1. JavaScript中的命令模式
    命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。JavaScript将函数作为一等对象,与策略模式一样,命令模式早已融入到了JavaScript语言中。运算块可以封装在普通函数中。函数作为一等对象,本身就可以被四处传递。
const bindClick = (button, func) => {
  button.onclick = func
}

const MenuBar = {
  refresh() {
    console.log('刷新菜单界面')
  }
}

const SubMenu = {
  add() {
    console.log('增加子菜单')
  },
  del() {
    console.log('删除子菜单')
  }
}

bindClick(button1, MenuBar.refresh)
bindClick(button2, SubMenu.add)
bindClick(button3, SubMenu.del)

2.18 备忘录模式

  1. 介绍
    随时记录一个对象的状态变化。随时可以恢复之前的某个状态(如撤销功能)。
  2. 代码演示
//备忘类
class Memento {
  constructor(content) {
    this.content = content
  }

  getContent() {
    return this.content
  }
}

//备忘列表
class CareTaker {
  constructor() {
    this.list = []
  }

  add(memento) {
    this.list.push(memento)
  }

  get(index) {
    return this.list[index]
  }
}

//编辑器
class Editor {
  constructot() {
    this.content =  null
  }

  setContent(content) {
    this.content = content
  }

  getContent() {
    return this.content
  }

  saveContentToMemento() {
    return new Memento(this.content)
  }

  setContentFromMemento(mement) {
    return this.content = mement.getContent()
  }
 }

 //测试代码
const editor = new Editor()
const careTaker = new CareTaker()

editor.setContent('1')
editor.setContent('2')
careTaker.add(editor.saveContentToMemento())
editor.setContent('3')
careTaker.add(editor.saveContentToMemento())
editor.setContent('4')
console.log(editor.getContent()) // 4
editor.setContentFromMemento(careTaker.get(1)) //撤销
console.log(editor.getContent()) // 3 
editor.setContentFromMemento(careTaker.get(0)) //撤销
console.log(editor.getContent()) // 2

2.19 中介者模式

  1. 介绍
    解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通讯,而不是互相引用。当一个对象发生改变时,只需要通知中介者对象即可。
  2. 中介者模式使网状的多对多关系变成了相对简单的一对多关系


    image.png
  3. 现实中的中介者
    ① 机场指挥塔
    ② 博彩公司
  4. 代码演示
class A {
  constructor() {
    this.number = 0
  }

  setNumber(num, m) {
    this.number = num
    if(m) {
      m.setB()
    }
  }
}

class B {
  constructor() {
    this.number = 0
  }

  setNumber(num, m) {
    this.number = num
    if(m) {
      m.setA()
    }
  }
}

//中介者
class Mediator {
  constructor(a, b) {
    this.a = a
    this.b = b
  }

  setB() {
    let num = this.a.number
    this.b.setNumber(num / 10)
  }

  setA() {
    let num = this.b.number
    this.a.setNumber(num + 5)
  }
}

const a = new A()
const b = new B()
const m = new Mediator(a, b)
a.setNumber(100, m)
console.log(a.number, b.number)

b.setNumber(100, m)
console.log(a.number, b.number)

2.20 访问者模式

  1. 介绍
    将数据操作与数据结构进行分离。

2.21 解释器模式

  1. 介绍
    描述语言语法如何定义,如何解释和编译。

3. 设计原则和编程技巧

3.1 设计原则概述

  1. UNIX/LINUX设计哲学》设计准则
    ① 小既是美。
    ② 每个程序只做一件事情。
    ③ 快速建立原型。
    ④ 舍弃高效率而取可移植性。
    ⑤ 避免强制性的图形化界面交互。
    ⑥ 让每个程序都成为过滤器。
    ⑦ 寻求90%的解决方案。
    注释:花20%的成本解决80的需求。
  2. 五大设计原则(SOLID)
    S - 单一职责原则
    O - 开放封闭原则
    L - 李氏置换原则
    I - 接口独立原则
    D - 依赖倒置原则
  3. 单一职责原则
    一个程序只做好一件事情。
  4. 开放封闭原则
    对扩展开放,对修改封闭。
    增加需求时,扩展新代码,而非修改已有代码。
  5. 李氏置换原则
    子类能覆盖父类。
    父类能出现的地方子类就能出现。
  6. 接口独立原则
    保持接口的独立,避免出现“胖接口”。
  7. 依赖倒置原则
    面向接口编程,依赖于抽象而不依赖于具体。

3.2 单一职责原则

  1. 简介
    就一个类、对象以及方法而言,应该仅有一个引起它变化的原因。
    注释:单一职责原则定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。
  2. 原则
    一个对象(方法)只做一件事情。
  3. 设计模式验证
    ① 代理模式
    图片预加载代理模式中,代理对象负责预加载职责,本体对象负责图片加载职责。
    ② 迭代器模式
    迭代器模式提供遍历访问聚合对象的职责。
    ③ 单例模式
    将创建对象与管理单例分别封装在两个方法中,两个方法相互独立互不影响。
    ④ 装饰者模式
    让类或者对象一开始只具有一些基础的职责,更多的职责在代码运行时被动态装饰到对象上面。装饰者模式可以为对象动态增加职责,从另一个角度来看, 这也是分离职责的一种方式。
  4. 应用场景
    ① 如果有两个职责总是同时变化,那就不必分离他们。即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们。
    ② 在方便性与稳定性之间要有一些取舍。具体是选择方便性还是稳定性,并没有标准答案,而是要取决于具体的应用环境。例如:jQueryattr 是个非常庞大的方法,既负责赋值,又负责取值,这对于jQuery的维护者来说,会带来一些困难,但对于jQuery的用户来说,却简化了用户的使用。
  5. 优缺点
    ① 优点
    按照职责把对象分解成更小的粒度,降低了单个类或者对象的复杂度,有助于代码的复用,也有利于进行单元测试。
    ② 缺点
    增加了编写代码的复杂度,也增大了这些对象之间相互联系的难度。

3.3 最少知识原则

  1. 简介
    最少知识原则(LKP)指一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体不仅包括对象,还包括系统、类、模块、函数、变量等。
  2. 减少对象之间的联系
    单一职责原则指导我们把对象划分成较小的粒度,提高对象的可复用性。但越来越多的对象之间可能会产生错综复杂的联系,如果修改了其中一个对象,很可能会影响到跟它相互引用的其他对象。
    最少知识原则要求我们尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。
  3. 设计模式验证
    ① 中介者模式
    增加一个中介者对象,让所有的相关对象都通 过中介者对象来通信,而不是互相引用。当一个对象发生改变时,只需要通知中介者对象即可。
    ② 外观模式
    外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。

3.4 开放-封闭原则

  1. 简介
    软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
  2. 原则
    当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。
  3. 实现方式
    通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。
    (1) 利用对象多态性
    利用对象的多态性来消除条件分支语句。
    (2) 放置挂钩
    在程序有可能发生变化的地方放置一个挂钩,挂钩的返回结果决定了程序的下一步走向。
    (3) 回调函数
    把一部分易于变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。
  4. 设计模式验证
    ① 观察者模式
    当有新的订阅者出现时,发布者的代码不需要进行任何修改;同样当发布者需要改变时,也不会影响到之前的订阅者。
    ② 模板方法模式
    子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽出来放到父类的模板方法里面;而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他的子类。
    ③ 策略模式
    策略模式将各种算法都封装成单独的策略类,这些策略类可以被交换使用。策略和使用策略的客户代码可以分别独立进行修改而互不影响。
    ④ 代理模式
    图片预加载示例中,代理函数proxyMyImage负责图片预加载,myImage图片加载函数不需要任何改动。
    ⑤ 职责链模式
    新增处理函数时,不需要改动原有的链条节点代码,只需要在链条中增加一个新的节点。

3.5 代码重构

  1. 提炼函数
    如果在函数中有一段代码可以被独立出来,那我们最好把这些代码放进另外一个独立的函数中。
var getUserInfo = function () {
  ajax('http:// xxx.com/userInfo', function (data) {
    console.log('userId: ' + data.userId);
    console.log('userName: ' + data.userName);
    console.log('nickName: ' + data.nickName);
  })
}

//改成
var getUserInfo = function () {
  ajax('http:// xxx.com/userInfo', function (data) {
    printDetails(data);
  });
};
var printDetails = function (data) {
  console.log('userId: ' + data.userId);
  console.log('userName: ' + data.userName);
  console.log('nickName: ' + data.nickName);
};
  1. 合并重复的条件片段
    如果一个函数体内有一些条件分支语句,而这些条件分支语句内部散布了一些重复的代码,那么就有必要进行合并去重工作。
var paging = function (currPage) {
  if (currPage <= 0) {
    currPage = 0;
    jump(currPage); // 跳转 
  } else if (currPage >= totalPage) {
    currPage = totalPage;
    jump(currPage);
  } else {
    jump(currPage);
  }
};

//改成
var paging = function (currPage) {
  if (currPage <= 0) {
    currPage = 0;
  } else if (currPage >= totalPage) {
    currPage = totalPage;
  }
  jump(currPage); // 把 jump 函数独立出来 
};
  1. 把条件分支语句提炼成函数
    在程序设计中,复杂的条件分支语句是导致程序难以阅读和理解的重要原因,而且容易导致一个庞大的函数。
var getPrice = function (price) {
  var date = new Date();
  if (date.getMonth() >= 6 && date.getMonth() <= 9) {
    return price * 0.8;
  }
  return price;
};

//改成
var isSummer = function () {
  var date = new Date();
  return date.getMonth() >= 6 && date.getMonth() <= 9;
};
var getPrice = function (price) {
  if (isSummer()) { // 夏天
    return price * 0.8;
  }
  return price;
};
  1. 合理使用循环
    在函数体内,如果有些代码实际上负责的是一些重复性的工作,那么合理利用循环不仅可以 完成同样的功能,还可以使代码量更少。
var createXHR = function () {
  var xhr;
  try {
    xhr = new ActiveXObject('MSXML2.XMLHttp.6.0');
  } catch (e) {
    try {
      xhr = new ActiveXObject('MSXML2.XMLHttp.3.0');
    } catch (e) {
      xhr = new ActiveXObject('MSXML2.XMLHttp');
    }
  }
  return xhr;
};
var xhr = createXHR();

//改成
var createXHR = function () {
  var versions = ['MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
  for (var i = 0, version; version = versions[i++];) {
    try {
      return new ActiveXObject(version);
    } catch (e) {
    }
  }
};
var xhr = createXHR();
  1. 提前让函数退出代替嵌套条件分支
    嵌套的条件分支语句绝对是代码维护者的噩梦。嵌套的条件分支往往是由一些深信“每个函数只能有一个出口的”程序员写出的。但实际上,如果对函数的剩余部分不感兴趣,那就应该立即退出。
var del = function (obj) {
  var ret;
  if (!obj.isReadOnly) { // 不为只读的才能被删除
    if (obj.isFolder) { // 如果是文件夹
      ret = deleteFolder(obj);
    } else if (obj.isFile) {
      ret = deleteFile(obj);
    }
  }
  return ret;
};

var del = function (obj) {
  if (obj.isReadOnly) {
    return;
  }
  if (obj.isFolder) {
    return deleteFolder(obj);
  }
  if (obj.isFile) {
    return deleteFile(obj);
  }
};
  1. 传递对象参数代替过长的参数列表
    有时候一个函数有可能接收多个参数,而参数的数量越多,函数就越难理解和使用。在使用的时候,还要小心翼翼,以免少传了某个参数或者把两个参数搞反了位置。
    这时我们可以把参数都放入一个对象内,不用再关心参数的数量和顺序,只要保证参数对应的 key 值不变就可以了。
  2. 尽量减少参数数量
    在实际开发中,向函数传递参数不可避免,但我们应该尽量减少函数接收的参数数量。
  3. 少用三目运算符
    如果条件分支逻辑简单且清晰,这无碍我们使用三目运算符;但如果条件分支逻辑非常复杂,我们最好的选择还是按部就班地编写 ifelse
  4. 合理使用链式调用
    经常使用jQuery的程序员相当习惯链式调用方法,在JavaScript 中,可以很容易地实现方法的链式调用,即让方法调用结束后返回对象自身。
    使用链式调用的方式可以省下一些字符和中间变量,但调试时非常不方便。如果我们知道一条链中有错误出现,必须得先把这条链拆开才能加上一些调试log或者增加断点。
    如果该链条的结构相对稳定,后期不易发生修改,那么使用链式调用无可厚非。但如果该链 条很容易发生变化,导致调试和维护困难,那么还是建议使用普通调用的形式。
  5. 分解大型类
    面向对象设计鼓励将行为分布在合理数量的更小对象之中。
  6. return退出多重循环
    假设在函数体内有一个两重循环语句,我们需要在内层循环中判断,当达到某个临界条件时退出外层的循环。我们大多数时候会引入一个控制标记变量或设置循环标记。
    这两种做法无疑都让人头晕目眩,更简单的做法是在需要中止循环的时候直接退出整个方法。
    如果在循环之后还有一些将被执行的代码,我们可以把循环后面的代码放到 return 后面,如果代码比较多,就应该把它们提炼成一个单独的函数。

4. 综合案例

4.1 模拟购物车

  1. 案例分析
    (1) 使用jQuery做一个模拟购物车示例。
    (2) 显示购物列表、加入购物车、从购物车删除
  2. 涉及到的设计模式
    (1) 创建型
    工厂模式、单例模式
    (2) 结构型
    装饰器模式、代理模式
    (3) 行为型
    观察者模式、状态模式、模板方法模式
  3. UML类图
    image.png
  4. 代码演示
    imooc-design-mode

5. 项目应用场景总结

  1. 单例模式
    mainboard为单例,是否可以不声明为类,直接字面量对象。
    ② 利用装饰器模式,给类添加管理单例的方法,与创建对象的方法分离。
    ③ 现有通过this._groupsMap缓存的方式使用单例模式替换。
  2. 装饰器模式
    app监控功能
    titleWidgetModel装饰类
  3. 代理模式
    BaseLayout/utils.js中的createZoomSelectComponent方法使用缓存代理。
  4. 职责链模式
    BaseLayout/utils.js中的createZoomSelectComponent方法使用职责链模式和缓存代理。
  5. 组合模式
    ① 布局与组件的关系可以使用组合模式。先创建整个表单的树结构,再逐层遍历,而不是生成单层布局-控件结构。
  6. 策略模式
    ① 参数面板两种动画。
    ② 目录跳转cpt/frm
  7. 状态模式
    ① 可放大组件的放大状态。
    ② 可选中组件的选中状态。
    ③ 参数面板展示隐藏状态。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容

  • javascript设计模式与开发实践 设计模式 每个设计模式我们需要从三点问题入手: 定义 作用 用法与实现 单...
    穿牛仔裤的蚊子阅读 4,027评论 0 13
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,077评论 1 32
  • 1. 设计模式概述 简介在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。即设计模式是在某种场合下对某个...
    nimw阅读 558评论 0 0
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,710评论 2 17
  • 我们总是在白天 期待着黑夜的降临 可是黑夜来了 我们却开始害怕起来了 幸福总会伪装成疼痛的煎熬 在某个深夜里敲门
    一堇阅读 795评论 7 14