美文网首页
JavaScript面向对象与设计模式

JavaScript面向对象与设计模式

作者: nimw | 来源:发表于2018-09-11 08:37 被阅读6次

    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. 状态模式
      ① 可放大组件的放大状态。
      ② 可选中组件的选中状态。
      ③ 参数面板展示隐藏状态。

    参考资料

    相关文章

      网友评论

          本文标题:JavaScript面向对象与设计模式

          本文链接:https://www.haomeiwen.com/subject/cmmegftx.html