装饰器模式

    装饰器模式(Decorator Pattern)允许向一个现有的对象 添加新的功能,同时又不改变其结构。这种类型的设计模式属于 结构型模式,它是作为现有的类的一个包装;

    这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名 完整性 的前提下,提供了 额外的功能

    装饰器模式的 重点在于不改变原有的 结构 和 功能,现在需要一个办法,在不改变函数源代码的情况下,能给 函数增加功能,这正符合开放封闭原则;

    总结要点:

        1 为对象 添加新功能;

        2 不改变原有的 结构 和 功能。

                生活中示例 : 手机壳: 不改变手机原本的功能,只是添加了一些新功能

js 中装饰器的类图

        添加了一个 设置红色边框的 新功能:

装饰器

   在实际开发中还有一种 很常见的做法 使用了装饰器模式:

        比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们 一般都会先保存好原先的 window.onload,把它 放入新的 window.onload 里执行:

常见的作法

        这种做法 非常常见, 需要好好掌握;

        比如 在 Vue 中, 对于 7 种操作数组的方法,我们 对它进行了功能的扩充:arrayMethods ⾸先继承了 Array ,然后对数组中所有能改变数组自⾝的方法, 对这 7中方法 进行重写。

        重写后的方法会先执⾏它们 本⾝原有的逻辑,并对能 增加 数组长度的 3 个方法 push、unshift、splice ⽅法做了判断,获取到 插如的值(也就是对新值进行响应式),然后把新添加的值变成⼀个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知。

 Vue

    又比如 在一些使用的 第三方库 中, 常常会用到这种模式 (vconsole npm 库):

保留原来的方法
添加一些新的功能,并执行原来的方法
添加一些新的功能,并执行原来的方法

   问题

        像上面那样处理会带来以下几个问题

        1. 需要维护 类似 _onload、_open、_send 这样的 中间变量,就目前来说 算是小事,但是如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多;

        2. this 的指向问题,在 window.onload 和 XMLHttpRequest 的例子中没有这个问题,因为它们执行的时候都是指向 window,但是在其他的例子中就会出现:

问题

       _getElementById 是一个全局函数,当调用一个全局函数时,this 是指向 window 的,而 document.getElementById 方法的内部实现需要使用 this 引用,this 在这个方法内预期是指向 document,而不是 window, 这是错误发生的原因;

        所以我们需要重新指定 this 的 指向:

重新指定 this 的 指向

    还有一种完美的方式给 函数动态增加功能 的方式:用 AOP (面向切面编程) 装饰函数

AOP 

    Function.prototype.before 接受一个函数当作参数,这个函数即为新添加的函数,它 装载了新添加的功能代码;

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

    同样的  Function.prototype.after 就是 后置装饰,原理一样;

    看个例子,重构上面的 _getElementById  代码:

AOP 

      通过 显示 的指定 this ( document.getElementById = XXX,  this 为 document),来动态指定 函数内部的 this。

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

防止原型污染

      用 AOP 动态改变函数的参数

            beforefn 和原函数 __self 共用一组参数列表 arguments,当我们在 beforefn 的函数体内改变 arguments 的时候,原函数 __self 接收的参数列表自然也会变化;

before

           一个实际的应用场景

                发送 ajax 请求:

ajax 请求

               如果需要添加 token 或者其他的参数 上面这么做 暂时没有问题;

               但是 每个请求都会发送 token, 并且如果将来把这个函数移植到其他项目上,或者把它放到一个开源库中供其他人使用,Token 参数都将是多余的;

                使用 AOP 动态更改函数参数:

动态更改函数参数

    AOP 的应用实例

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

        1. 数据统计上报系统

                场景 :点击按钮, 打开浮层 并且 进行上报

场景 

                一般做法

   一般做法

              这种做法出现的问题就是 在 showLogin 函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处却被 耦合 在一个函数里;

              使用 AOP 分离 之后, 隔离了 showLogin  和  log 这两个方法:

AOP 分离 

        2. 插件式表单校验

1

          formSubmit 函数在此处承担了两个职责,除了提交 ajax 请求之外,还要验证用户输入的合法性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性;

2

            现在的代码已经有了一些改进,我们把校验的逻辑都放到了 validata 函数中,但 formSubmit 函数的内部还要计算 validata 函数的返回值,因为返回值的结果表明了是否通过校验;

3

            最终的 这段代码,使 validata 和 formSubmit 完全分离开来。改写 Function.prototype.before,如果 beforefn 的执行结果返回 false,表示不再执行后面的原函数;

    注意:

       1 函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失;

注意

        2 这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响;

        个人感觉 : 如果是重写原型的话,还是可以是用  非 AOP 形式, 这种 在很多库中都是这么使用的; 如果有一些 业务相关, 系统设计之类的 ,可以使用 AOP 的这种形式;


    ES7 装饰器

        配置环境

            npm install babel-plugin-transform-decorators-legacy --save-dev

        配置 .babelrc 文件

        装饰类

 装饰类
装饰类

                带参数的装饰器

             示例 mixin

     装饰方法

            1. 属性的限制:

  装饰方法1 

        2. 添加新的功能

  装饰方法2

core-decorators  库

        详见 :https://github.com/jayphelps/core-decorators

        第三方开源 lib , 提供常用得装饰器;

设计原则验证

    将 现有对象装饰器 进行分离,两者独立存在;

    符合开放封闭原则。


文中参考 和 摘抄

    JavaScript 设计模式与开发实践;

你可能感兴趣的:(装饰器模式)