前言

@decorator 装饰器是 es7 更新的提案,是一种与类相关的语法,用来注释或修改类和类的方法,是在装饰器模式的基础上产生的。装饰器是过去几年中js最大的成就之一,已是ES7的标准特性之一。

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为。 通俗的讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。

常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器

装饰器的写法:普通装饰器(无法传参) 、 装饰器工厂(可传参,下面的内容都将基于工厂模式)

先来了解一下普通装饰器的写法,工厂模式的写法在后面会看到很多,就不在这里介绍了

function Decorator(targetClass: any) {
  console.log(targetClass);
  targetClass.prototype.msg = "装饰器注入的实例属性";
  targetClass.func = function () {
    console.log("装饰器注入的方法");
  };
}

@Decorator
class Animals {
  constructor() {}
}

const animals: any = new Animals();
// @ts-ignore
Animals.func();
console.log(animals.msg);

可以直接再类上添加静态属性和静态方法,也可以通过prototype来添加实例方法

运行结果如下(如果使用TS来写代码,Node环境下需要配置typescript环境,这里我直接使用deno运行)

image-20210822213624908

类装饰器

上面的普通装饰器的示例就是一个类装饰器,这里我们将它重构一下,使用工厂模式来实现

function Decorator(params: any) {
  return function (targetClass: any) {
    targetClass.prototype.msg = `来自装饰器注入的消息${params}`;
  };
}

@Decorator("args")
class Animals {
  constructor() {}
}

const animals: any = new Animals();
console.log(animals.msg);

实现方式类似于柯里化

image-20210822225819796

使用装饰器的时候传递参数,在使用一些Node框架的时候经常遇到,类似于@Controller('/users'),通过装饰器指定controller的路由,这种情境下使用普通的装饰器函数已经无法满足,而工厂模式则可以轻而易举的完成这个任务。

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数; 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明;所以装饰器除了注入新的属性,也可以用来重载类的属性和方法,但是必须重载所有的属性和方法

属性装饰器

顾名思义,属性装饰器用来修饰类的属性,可以用在类的属性、方法、get/set 函数中,属性装饰器接收三个参数,目标类、被装饰的属性key、被装饰的属性描述,例如我们可以通过装饰器来将属性变为只读

function readonly() {
  return function (
    target: unknown,
    key: string,
    descriptor: PropertyDescriptor
  ) {
    console.log(target, key, descriptor);
    console.log(descriptor.value.toString());
    descriptor.writable = false;
    return descriptor;
  };
}

class Person {
  constructor() {}

  @readonly()
  sayHi() {
    console.log("Hi");
  }
}

const person = new Person();
person.sayHi();
person.sayHi = function () {};

除了修改方法描述,还可以使用target[key]的形式来获取属性值或者修改属性值。(修饰方法时可以通过descriptor.value来获取函数体,后面会用到)

image-20210823220439051

通过只读修饰之后的属性再修改属性值的时候就会报错。属性装饰器装饰方法的时候可以用来实现日志操作以及方法的拦截

修饰方法的装饰器最后必须返回属性描述descriptor

参数装饰器

参数装饰器用来装饰方法中的形参,装饰参数时接收三个参数:目标类、方法名、参数在arguments中的索引

function LogParams(params: string) {
  return function (target: unknown, methodName: string, index: number) {
    console.log(target, methodName, index);
    console.log(`监听${params}`);
  };
}

class Person {
  constructor() {}

  sayHi(@LogParams("user") user: string) {
    console.log("Hi", user);
  }
}

const person = new Person();
person.sayHi("king");

参数装饰器只能用来监视一个方法的参数是否被传入,并不能做太多的处理,所以并不常用

image-20210823115609344

装饰器的妙用

防抖&节流

节流防抖使我们日常开发中经常使用的性能优化的手段,之前的使用都需要封装一层函数,看起来也不舒服,现在有了装饰器,我们可以非常“爽”地进行防抖和节流的优化

// 节流
const throttle = (time: number) => {
  let prev = new Date().getTime();
  return (target: unknown, name: string, descriptor: PropertyDescriptor) => {
    // 前面的示例中说到过,通过descriptor.value获取函数体
    const func = descriptor.value;
    if (typeof func === "function") {
      descriptor.value = function (...args: any[]) {
        const now = new Date().getTime();
        if (now - prev > time) {
          func.apply(this, args);
          prev = new Date().getTime();
        }
      };
    }
  };
};

// 防抖
const debounce = (time: number) => {
  let timer: number;
  return (target: unknown, name: string, descriptor: PropertyDescriptor) => {
    const func = descriptor.value;
    if (typeof func === "function") {
      descriptor.value = function (...args: any[]) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
          func.apply(this, args);
        }, time);
      };
    }
  };
};

使用起来也非常的舒服,比如组件要监听滚动事件,我们就可以直接在绑定的函数上使用装饰器

class App extends React.Component {
    componentDidMount() {
        window.addEveneListener('scroll', this.scroll);
    }
    componentWillUnmount() {
        window.removeEveneListener('scroll', this.scroll);
    }
    @throttle(50)
    scroll() {}
}

类型校验

类型校验主要应用于JavaScript,typescript自带类型检验,所以不太有必要使用

const validate = (type) => (target, name) => {
    if (typeof target[name] !== type) {
        throw new Error(`TypeError: attribute ${name} must be ${type} type`)
    }
}
class Form {
    @validate('string')
    static name = 111 // TypeError: attribute name must be ${type} type
}

前端小白