zoukankan      html  css  js  c++  java
  • Javascript-设计模式_装饰者模式

    前言

    在js函数开发中,想要为现有函数添加与现有功能无关的新功能时,按普通思路肯定是在现有函数中添加新功能的代码。这并不能说错,但因为函数中的这两块代码其实并无关联,后期维护成本会明显增大,也会造成函数臃肿

    比较好的办法就是采用装饰器模式。在保持现有函数及其内部代码实现不变的前提下,将新功能函数分离开来,然后将其通过与现有函数包装起来一起执行

     

     

    装饰者模式

    描述

    装饰者模式是一种为函数或类增添特性的技术,它可以让我们在不修改原来对象的基础上,为其增添新的能力和行为。它本质上也是一个函数(在javascipt中,类也只是函数的语法糖),装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链

     

    继承的缺陷

    "封装,继承,多态"是面向对象的三大特点,开发过程中,我们都不希望某些类一开始就特别庞大,一次性包含太多职责,这时就不得不去面对三大特性的权衡

    在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:

    • 会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变

    • 继承这种功能复用方式通常被称为“白箱复用“,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性

    • 在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长。比如现在有4种型号的相机,我们为每种相机都定义了一个单独的类。现在要给每种相机都装上镜头、滤镜和挂带这3种配件。如果使用继承的方式来给每种相机创建子类,则需要4×3=12个子类

    但是如果把镜头、滤镜和挂带这些对象动态组合到相机上面,则只需要额外增加3个类

    这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓

     

    一系列问题

    不改动原函数的情况下,给该函数添加些额外的功能

    保存原引用

    window.onload = function() {
        console.log('onload')
    };
    
    var _onload = window.onload || function() {}
    
    window.onload = function() {
        _onload()
        console.log('_onload')
    }
    

    问题

    • 必须维护中间变量

    • 可能遇到this被劫持问题 在window.onload的例子中没有这个烦恼,是因为调用普通函数_onload时,this也指向window,跟调用window.onload时一样

    this被劫持

    const _getElementById = document.getElementById
    document.getElementById = function(id) {
        console.log('_getElementById')
        return _getElementById(id)
    }
    
    return _getElementById(id)  // 报错“Uncaught TypeError: Illegal invocation”
    

    因为_getElementById是全局函数,当调用全局函数时,this是指向window的,而document.getElementById中this预期指向document

     

    面向对象的装饰者

    一个打飞机游戏,飞机一开始火力是普通子弹,升2级后是导弹,3级后是核导弹,用装饰者的写法是:

    class Plane{
      fire(){
        console.log('发送普通子弹')
      }
    }
    
    class Missibe{
      constructor(plane) {
        this.plane = plane
      }
      fire(){
        this.plane.fire()
        console.log('发送导弹')
      }
    }
    
    
    class Atom{
      constructor(plane) {
        this.plane = plane
      }
      fire(){
        this.plane.fire()
        console.log('发送核导弹')
      }
    }
    
    const plane = new Plane()
    const missibePlane = new Missibe(plane)
    const atom = new Atom(missibePlane)
    
    atom.fire()
    

    这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象

     

    AOP装饰函数

    Aop又叫面向切面编程,其中“通知”是切面的具体实现,分为before(前置通知)、after(后置通知)、around(环绕通知),在js中AOP是一个被严重忽视的技术点

    /**
     * 让新添加的函数在原函数之前执行(前置装饰)
     */
    Function.prototype.before = function(beforefn) {
        var _self = this
        return function() {
          	// 新函数接收的参数会被原封不动的传入原函数
            beforefn.apply(this, arguments)
            return _self.apply(this, arguments)
        }
    }
    
    /**
     * 让新添加的函数在原函数之后执行(后置装饰)
     */
    Function.prototype.after = function(afterfn) {
        const _self = this
        return function() {
            const ret = _self.apply(this, arguments)
            afterfn.apply(this, arguments)
            return ret
        }
    }
    
    document.getElementById = document.getElementById.before(function() {
        console.log('getElementById')
    })
    

    Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果

    通过Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持

     

    应用场景

    用AOP装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写,还是在框架层面,我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高复用性的系统

    • 用aop动态改变函数的参数

      假设在使用一串模板方法,在异步请求前,你给before传入一段函数

      let fnAjax=function(param){
        console.log(arguments)
        console.log(param)
      }
      
      fnAjax=fnAjax.before(function(param){
        param.username='djtao'
      })
      
      fnAjax({password:123456})
      // Object {password: 123456, username: "djtao"}

      可以用它来为你的ajax请求预处理参数。比如带个token什么的

    • 插件式的表单校验

      假设点击提交时都要校验,如果把validate写在submit处理函数中,这段代码没有任何可复用性,可以改写一下before

      Function.prototype.before = function(fn) {
        const _this = this
        return function() {
          if(fn.apply(this,arguments) === false) return
          return _this.apply(this, arguments)
        }
      }
      
      const validate = function(params) {
        const {username, password} = params
        if(username === '') {
          alert('用户名不得为空')
          return false
        }
        if(password === '') {
          alert('密码不得为空')
          return false
        }
      }
      
      login = login.before(validate)

      在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系,formSubmit = formSubmit.before(validata)这句代码,如同把校验规则动态接在formSubmit函数之前,validata成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分开维护这两个函数。再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式,用在不同的项目当中

    • 统计数据上报

      现有一个按钮,功能已经开发完成

      const btnFn = () => {
        //...
      }
      
      document.querySelector('#btn', btnFn)

      这时需要加多一个点击统计功能,如果直接在btnFn继续写统计,是不合适的。这是两个层面的功能,却被耦合在一个函数里,这时可以考虑aop分离

    • const notePoint = ()=> {
        // 统计逻辑
      }
      
      btnFn = btnFn.after(notePoint)

     

    和代理模式的区别

    装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求

    代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为

    换句话说,代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时,代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链

    在虚拟代理实现图片预加载的例子中,本体负责设置img节点的src,代理则提供了预加载的功能,这看起来也是“加入行为”的一种方式,但这种加入行为的方式和装饰者模式的偏重点是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为,而代理做的事情还是跟本体一样,最终都是设置src。但代理可以加入一些“聪明”的功能,比如在图片真正加载好之前,先使用一张占位的loading图片反馈给客户

     

    小结

    使用装饰者模式可以让我们为原有的类和函数增添新的功能,并且不会修改原有的代码或者改变其调用方式,因此不会对原有的系统带来副作用,也不用担心原来系统会因为它而失灵或者不兼容

     
  • 相关阅读:
    【每日一题-leetcode】 47.全排列 II
    【每日一题-leetcode】46.全排列
    【每日一题-leetcode】 77.组合
    【每日一题-leetcode】105.从前序与中序遍历序列构造二叉树
    【每日一题-leetcode】297.二叉树的序列化与反序列化
    【读书笔记】《淘宝技术这十年》
    python第17天-网络复习
    python编码风格
    python第16天-网络4
    python第15天-网络3
  • 原文地址:https://www.cnblogs.com/zhazhanitian/p/13573787.html
Copyright © 2011-2022 走看看