TS的装饰器你再学不会我可就要报警了哈🤣🤣🤣

5,379 阅读6分钟

最近在学习 NestJs,也可以理解为我要开始接触后端了,在这段你的时间里我接触到了很多新的概念,例如控制反转和依赖注入,NestJs 大量借鉴了 Angularspring 的思想,所以这也就是一些 vue 或者 react 开发者开始学习 NestJs 会有些不适应,它通过 @Controller@Injectable 装饰器声明的类会被 NestJs 扫描,创建对应的对象并添加到一个容器里,最终形成一个类似于 Webpack 模块图一样,这些所有的对象会根据构造器里声明的依赖自动注入,也就是 DI(dependency Inject),这种思想叫做 IOC(Inverse Of Control),后者就是控制反转,是一种设计模式,而前者就是依赖注入,是一种具体的实现方式。

在官方文档中有这样的一句话:

Nest is built around the strong design pattern commonly known as Dependency injection. We recommend reading a great article about this concept in the official Angular documentation.

现在 JavaScript 中的装饰器提案已经进入到 stage 3 阶段了,相信不久的将来就算你不用ts也可以使用上了,赶紧学起来吧!!!

装饰器(Decorator)

装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,装饰者使用 @+函数名 形式来修改类的行为。

常见的装饰器有:

  • 类装饰器;
  • 属性装饰器;
  • 方法装饰器;
  • 参数装饰器;

而装饰器的写法又分为两种它们分别是:

  • 普通装饰器(无法传参);
  • 装饰器工厂(可传参);

类装饰器(普通装饰器)

类装饰器声明在类声明之前,类装饰器应用于类的构造函数,可用于观察、修改或替换来定义,它不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类上)。

具体代码如下所示:

function Controller(params: any) {
  console.log(params === Services); // true
  params.prototype.moment = "777";
}

@Controller
class Services {
  constructor() {}

  getMoment() {}
}

const s: any = new Services();
console.log(s.moment); // 777

在上面的代码中,我们可以得出以下结论:

  • Controller 装饰器声明在类声明之前,并且不携带分号;
  • Controller 函数中的参数 params 就是 Services 类;
  • 可以在不修改类的前提上扩展类和方法;

在上面的例子中,它是一个普通的装饰器,它无法传参,传参会报错:

image.png

类装饰器(装饰器工厂)

如果你想定义一个装饰器工厂,那么它很简单,它也是一个函数,和前面的不同的是,它又返回一个函数,这个被返回的函数接收一个参数,这个参数就是被装饰的类,具体代码如下所示:

function Controller(params: string) {
  console.log(params); // moment

  return function (target) {
    console.log(target === Services); // true
    target.prototype[params] = 777;
  };
}

@Controller("moment")
class Services {
  constructor() {}

  getMoment() {}
}

const s: any = new Services();
console.log(s.moment); // 777

而函数 Controller 中的 params 参数正是调用装饰器时传入的参数。

属性装饰器

属性装饰器就比较简单了,直接上代码吧:

function Controller(params) {
  return function (target: any, attribute: any) {
    console.log(target); 
    console.log(attribute);
    console.log(params);
  };
}

class Services {
  @Controller("nba")
  public moment: number;
}

const s: any = new Services();

最终的输出结果如下图所示:

image.png

方法装饰器

方法装饰器的表达式将在运行时作为函数调用,带有以下三个参数:

  • 静态成员类的构造函数或者实例成员类的原型;
  • 成员的名称;
  • 成员的属性描述符;

先看以下代码:

function Controller(params) {
  return function (target: any, attribute: any, descriptor: any) {
    console.log(target);
    console.log(attribute);
    console.log(descriptor);
  };
}

class Http {
  constructor() {}

  @Controller("t")
  getDate() {}
}

最终的输出结果如下图所示:

image.png

在这里我们可以通过 descriptor.value 来获取当前被装饰的方法,在这里我可以对该方法进行修改或者拦截,具体代码如下所示:

function Controller(params: any) {
  return function (target: any, attribute: any, descriptor: any) {
    const method = descriptor.value;

    descriptor.value = function (value: string) {
      method.call(this, value);

      return value.toLocaleUpperCase();
    };
  };
}

class Http {
  constructor() {}

  @Controller("sss")
  getDate() {
    console.log(666666);
  }
}

const foo: any = new Http();

console.log(foo.getDate("nba"));

在这段代码中,方法装饰器修改了当前的方法,对方法进行扩展,通过 method 来对当前方法进行保存,最后进行调用目的是为了不改变原有的函数体。

image.png

方法参数装饰器

方法参数装饰器表达式会在运行时当做函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入下列的三个参数:

  • 静态成员类的构造函数或者实例成员类的原型;
  • 方法的名称;
  • 参数在函数参数列表中的索引;

具体代码如下所示:

function Controller(params: any) {
  return function (target: any, method: any, index: number) {
    console.log(params); // cba
    console.log(target); // Http 类
    console.log(method); // 方法名
    console.log(index); // 参数索引
  };
}

class Http {
  constructor() {}

  getDate(@Controller("cba") value: any) {
    console.log(value); // nba
  }
}

const foo: any = new Http();

foo.getDate("nba");

最终的输出结果如下图所示:

image.png

装饰器执行顺序

装饰器的执行顺序很简单,但是我看到网上的讲解有说是这样的顺序的,如下:

  1. 属性装饰器;
  2. 方法参数装饰器;
  3. 方法装饰器;
  4. 类装饰器;

其实这个说法是错误的,装饰器的执行顺序是谁先执行完就先调用谁(方法参数是从后到前,方法也是),具体代码如下所示:

function a(params: any) {
  console.log("类装饰器");
}
function b(params: any) {
  return function (target: any, name: any) {
    console.log("属性装饰器");
  };
}
function c(params: any) {
  return function (target: any, name: any) {
    console.log("方法装饰器");
  };
}
function d(params: any) {
  return function (target: any, name: any, index: number) {
    console.log("方法参数装饰器");
  };
}

function e(params: any) {
  return function (target: any, name: any, index: number) {
    console.log("方法参数装饰器1");
  };
}

@a
class Http {
  constructor() {}
  @c("")
  getDate(@d("") value, @e("") v: any) {}
  @b("")
  public moment: number | undefined;
}

const foo: any = new Http();

foo.getDate("nba");

这段代码的最终输出结果如下所示:

image.png

看,现在不是属性装饰器执行完了吧,这个主要是看 JavaScript 代码从上往下执行,谁先执行完就调用谁,在整段代码中,整个类最后执行完,这也就是类最后调用的原因了。

参考文献

typescript官网

结尾

很久之前就看到了一句很好的一句话,现在突然想起来,现在分享给大嘎:

做人要接受自己的世俗,然后你会发现你坚定的不一定是真,你崇尚的不一定是善,你热爱的不一定是美,你的才华并不能撑起你满腔的抱负,你并不是世界的中心,你的征途也并不是星辰大海,你只是个平庸且世俗的人,你唯一能做的,就是珍惜并抓住眼前的所有,然后美其名曰幸福。

本文正在参加「金石计划」