Javasript设计模式-装饰器模式

77 阅读6分钟

装饰器模式属于结构性,前面说,结构型模式描述如何将类或者对象结合在一起形成更大的结构。那么装饰器模式呢,属于其中一小部分,就是对原对象进行包装扩展,这个过程并不改变原对象,从而实现产品经理的复杂需求。

下面还是说一个小需求来介绍装饰器模式:

这里有一段别人的代码:

//别人代码
let eg = () => {
	alert(1)
};

需求就是,你要在执行eg函数的时候,顺便再弹出一个2。

这里正常改的话,可能就写成这样:

//修改代码
let eg = () => {
	alert(1)
	alert(2)
};

但,我们说这样写其实就违反了开闭原则,而且,如果这里的alert(1)并不是一句简单的执行语句,而是一堆复杂的逻辑代码,而alert(2)也并不是简单的alert(2),这个时候,怎么办呢?

我们可以这么写:

//扩展函数
const _eg = eg;
eg = () => {
	_eg();
	alert(2);
}

我们可以新建一个变量_eg先将原函数保存一下原函数的引用,然后扩展一下eg,执行刚保存的原函数_eg,然后将自己的功能扩展在原函数的下面。

回顾装饰器模式的定义:“在不改变原对象的基础上,对原对象进行包装扩展”,我们发现这里就是一个装饰器模式的应用。

这里我扯一个设计思想——AOP(Aspect Oriented Programing):

为了更好的介绍AOP,我们再把它跟我们平时比较熟悉的OOP对比下:

我们常说要用OOP(Object Oriented Programing)——面向对象编程,那么AOP呢,就是面向切面编程,这两者并不是一个互斥的关系,他们的区别是:

OOPAOP
封装封装的是方法和属性封装的是业务
最小操作单元对象切面
特点属性和方法都要通过对象才能调将主业务和通用业务区分,将通用业务划分为切面
特点把系统看成多个对象的交互把系统分为不同的关注点,或者称之为切面

为什么要扯AOP过来呢,因为AOP跟装饰器模式是有点像的,接下简单来说明下:

AOP在Java Spring的应用中,有三种通知,before(前置通知)、after(后置通知)、arround(环绕通知)。

什么意思呢?具体我们用ES5实现下其中一种通知,“before”就明了了。

Function.prototype.before = function (beforefn) {
  const __self = this;
  return function () {
    beforefn.apply(this, arguments);
    return __self.apply(this, arguments);
  };
};

可以看到,我们在Function的原型里加了一个before,那其实我们前面那个实现装饰器模式的例子就可以这么写:

function  myFn(){
  alert(2)
}

eg=eg.before(myFN)
eg()

那可能有的同学会说,你这样用js去实现AOP,污染原型嘛!

那我们不写在原型上:

 const before=function(fn,beforeFn){
    return function(){
      beforeFn.apply(this,arguments);
      return fn.apply(this,arguments);
    }
  }
  cosnt eg = function(){alert(2)};
  eg=before(eg,function(){alert(1)});
  eg()

对吧,这样我们就用一种相对比较优雅的方式实现了这个扩展需求。

这种方式其实在前端的应用很多,我们可以随便头脑风暴下都可以应用到哪些方面,想想有哪些不需要改变主业务,只是处理一些通用业务的部分,或者说,不改变原对象,仅仅只是对原对象进行功能的扩展。

那其实,ES7已经支持装饰器的语法糖,写起来更加清爽简洁(但是装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升),接下来我们一起看一看。

装饰器

装饰器是es7 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。它也是实现 AOP编程的一种重要方式。接下来看一个实例。

还是刚刚消息弹窗的例子,我现在要完善这个弹窗,就是我这个按钮想变成一次性的,点击完后就不允许再次点击,那么这个需求很简单,就是在按钮点击后,将按钮的disable置否。

我们可以这么实现:

function disableBtn(target: any, name: string, descriptor: PropertyDescriptor) {
  const oldValue = descriptor.value;
  descriptor.value = function (status: string) {
    const btn = document.querySelector(`button[data-status="${status}"]`);
    btn.setAttribute("disabled", "true");
    oldValue.call(this, status);
  };
}


class ModalFactory {
  dom: HTMLElement;
  constructor(dom1: HTMLElement) {
    this.dom = dom1;
  }
  modal: any = null;
  
 @disableBtn             //******************使用的地方在这里~!*******************//
 create(status: MType) {

    switch (status) {
      case MType.success:
        this.modal = new SuccessModal();
        break;
      case MType.warning:
        this.modal = new WarningModal();
        break;
      case MType.error:
        this.modal = new ErrorModal();
        break;
      default:
        break;
    }
    this.dom.className = this.modal.className;
  }
}

看看效果:

编辑切换为全宽

添加图片注释,不超过 140 字(可选)

这样,我们就可以很方便地去扩展功能啦。

当然,由于浏览器还不支持装饰器,现在使用还是需要一些额外操作,你需要开启 tsconfig.json里的实验选项:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

这里的disableBtn是一个方法装饰器,相关的还有类装饰器、属性装饰器、参数装饰器、访问装饰器。

类装饰器、方法装饰器、属性装饰器、参数装饰器

类装饰器、方法装饰器、属性装饰器、参数装饰器,他们各自的参数不同,我们可以通过查看其类型得知这些参数是什么。

下图为ts中每种装饰器的类型定义:

//类装饰器
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
//属性装饰器
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
//方法装饰器
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
//参数装饰器 
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,parameterIndex: number) => void;

打印不同装饰器的每一个参数:

//类
const aa: ClassDecorator = (target) => {
  console.warn("------------------------------------类装饰器");

  console.log(target);
};
@aa
class A {}

//方法
const bb: MethodDecorator = (target, name, descriptor) => {
  console.warn("------------------------------------方法装饰器");

  console.log(target);
  console.log(name);
  console.log(descriptor);
};

class B {
  @bb
  method() {}
}

//属性
const cc: PropertyDecorator = (target, name) => {
  console.warn("-----------------------------------属性装饰器");

  console.log(target);
  console.log(name);
};

class C {
  @cc
  name: string;
}

//参数
const dd: ParameterDecorator = (target, name, index) => {
  console.warn("------------------------------------参数装饰器");

  console.log(target);
  console.log(name);
  console.log(index);
};

class D {
  method(@dd param: string) {}
}

编辑切换为居中

添加图片注释,不超过 140 字(可选)

可以看到,类装饰器的参数只有一个,就是它本身。

方法装饰器的参数有三个,分别为当前装饰的函数的原型、当前修饰的函数名称以及一个description,里面可以看到可以拿到一些当前修饰的函数的控制权,那么有了这些控制权,我们可以做很多事。

属性装饰器的参数就是修饰的函数原型和修饰的属性名。

参数装饰器的参数就是修饰的函数原型和修饰的方法名以及参数索引。

其实我们平时最常用的就是方法装饰器,我们刚刚说,方法装饰器里可以拿到当前修饰函数的控制权,可以做很多事,就比如说刚刚的disableBtn,我们先通过第三个参数拿到的旧的函数值,然后添加一些自己要扩展的功能,再返回,这样就完成了一个不破坏原有结构的扩展。

装饰器工厂

还是消息提示框的例子,我们再添一个需求,就是根据点击的不同按钮,创建对应状态的消息提示框,然后消息提示框展示一秒后消失。

要根据点击的不同按钮,创建对应状态的消息提示框,我们首先创建一个createModal的装饰器工厂:

export const createModal = (status: MType) => {
  return (target: any, name: string, descriptor: PropertyDescriptor) => {
    const oldValue = descriptor.value;
    descriptor.value = function (status: string) {
      const div = document.createElement("div");
      div.className = `modal ${status}`;
      div.innerHTML = `<header>${status}</header>`;
      document.body.appendChild(div);
      oldValue.call(this, status);
    };
  };
};

可以看到,我们传递了一个状态参数进来,然后根据不同的状态返回相应的装饰器,相当于将正常的装饰器函数外面包了一层,可以用来传参。

接着我们写下一秒消失的装饰器:

export const after = (
  target: any,
  name: string,
  descriptor: PropertyDescriptor
) => {
  const oldValue = descriptor.value;
  descriptor.value = function (status: string) {
    setTimeout(() => {
      const oModal: Element = document.querySelector(".modal");
      document.body.removeChild(oModal);
    }, 1000);
    oldValue.call(this, status);
  };
};

接着我们将这两个装饰器叠加在修饰方法上:

class ModalFactory {
  dom: HTMLElement;
  constructor(dom1: HTMLElement) {
    this.dom = dom1;
  }
  modal: any = null;

  @before(MType.success)
  @after
  create(status: MType, title: string) {
      //.......
    }
  }
}

看看效果

编辑切换为全宽

添加图片注释,不超过 140 字(可选)

这样我们就实现需求啦。

装饰器模式的应用

前文说让大家想想装饰器模式在前端的应用,不知道大家想起了多少呢?

其实在装饰器模式在前端的应用很多,比如说,localStorage设置过期时间、路由守卫、请求公共参数等,其中也有很著名的应用例如React高阶组件(HOC):

import React from 'react';

export default Component => class extends React.Component {
  render() {
    return <div style={{cursor: 'pointer', display: 'inline-block'}}>
      <Component/>
    </div>
  }
}

这里这个装饰器(高阶组件)接受一个 React 组件作为参数,然后返回一个新的 React 组件。可以看出,就是包裹了一层 div,添加了一个 style,就这么修饰一下,以后所有被它装饰的组件都会具有这个特征。

我们前面说,ts的装饰器是AOP思想在前端的应用,AOP有一个特点是就是区分开主要业务和通用业务,着眼于处理通用业务。而装饰器就是用来封装通用业务,并不依赖于其他任何逻辑,这也提醒我们,其实很多通用业务我们没必要自己去造轮子嘛,说不定网上已经有现成的实现了。

接下来给大家推荐一个网站:github.com/jayphelps/c…

里面有一些使用频率较高的装饰器,里面的实现也都是简单又通用的功能。非常推荐有兴趣的同学去看看。

红信圈-社交营销裂变工具1️⃣让别人帮你分享微信朋友圈;2️⃣让别人主动加你微信

4.1分

下载