Skip to content

装饰器

个人理解 装饰器就是个扩展,他把那些类,方法,访问符号,属性传递到装饰器里面,然后装饰器里面在对他们改变

ECMAScript 的装饰器提案到现在还没有定案,所以我们直接看 TS 中的装饰器。同样在 TS 中,装饰器仍然是一项实验性特性,未来可能有所改变,所以如果你要使用装饰器,需要在 tsconfig.json 的编译配置中开启 experimentalDecorators,将它设为 true。

基础

装饰器是一种新的声明,它能够作用于类声明、方法、访问符、属性和参数上。使用@符号加一个名字来定义,如@decorat,这的 decorat 必须是一个函数或者求值后是一个函数,这个 decorat 命名不是写死的,是你自己定义的,这个函数在运行的时候被调用,被装饰的声明作为参数会自动传入。要注意装饰器要紧挨着要修饰的内容的前面,而且所有的装饰器不能用在声明文件(.d.ts)中,和任何外部上下文中(比如 declare,关于.d.ts 和 declare,我们都会在讲声明文件一课时学习)。比如下面的这个函数,就可以作为装饰器使用:

js
function setProp(target) {
  // todo
}

@setProp
class Person {}

// 这里面的target 就是Person这个类

先定义一个函数,然后这个函数有一个参数,就是要装饰的目标,装饰的作用不同,这个 target 代表的东西也不同,下面我们具体讲的时候会讲。定义了这个函数之后,它就可以作为装饰器,使用@函数名的形式,写在要装饰的内容前面。

装饰器工厂

装饰器工厂也是一个函数,它的返回值是一个函数,返回的函数作为装饰器的调用函数。如果使用装饰器工厂,那么在使用的时候,就要加上函数调用,如下:

工厂和装饰器的区别在于 工厂后面有() 装饰器后面无()

js
function setProp(params) {
  return function (target) {
    // todo...
  };
}

@setProp()
class Person {}

装饰器组合

装饰器可以组合,也就是对于同一个目标,引用多个装饰器

js
// 可以写在一行
@setName @setAge  target
// 或者换行
@setName
@setAge

注意

多个装饰器的执行顺序:

  • 装饰器工厂从依次执行,但是只是用于返回函数 但不调用函数

  • 装饰器函数由执行,也就是执行工厂函数的返回函数

我们用下面两个装饰器工厂为例

js
function setName() {
  console.log("get setName");
  return function (target) {
    console.log("setName");
  };
}
function setAge() {
  console.log("get setAge");
  return function (target) {
    console.log("setAge");
  };
}
@setName()
@setAge()
class Test {}
// 打印出来的内容如下:
/**
 'get setName'
 'get setAge'
 'setAge'
 'setName'
*/

可以看到,多个装饰器,会先执行装饰器工厂函数获取所有装饰器,然后再从后往前执行装饰器的逻辑。

装饰器求值

类的定义中不同声明上的装饰器将按以下规定的顺序引用:

  • 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个实例成员;

  • 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个静态成员;

  • 参数装饰器应用到构造函数;

  • 类装饰器应用到类。

类装饰器

类装饰器在类声明之前声明,要记着装饰器要紧挨着要修饰的内容,类装饰器应用于类的声明。

类装饰器表达式会在运行时当做函数被调用,它由唯一一个参数,就是装饰的这个类。

js
let sign = null;
function setName(name: string) {
  return function (target: Function) {
    sign = target;
    console.log(target.name);
  };
}
@setName("lison") // Info
class Info {
  constructor() {}
}
console.log(sign === Info); // true
console.log(sign === Info.prototype.constructor); // true

可以看到,我们在装饰器里打印出类的 name 属性值,也就是类的名字,我们没有使用 Info 创建实例,控制台也打印了"Info",因为装饰器作用与装饰的目标声明时。而且我们将装饰器里获取的参数 target 赋值给 sign,最后判断 sign 和定义的类 Info 是不是相等,如果相等说明它们是同一个对象,结果是 true。而且类 Info 的原型对象的 constructor 属性指向的其实就是 Info 本身。

这样通过装饰器,我们就可以修改类的原型对象和构造函数

js
function logClass(params: any) {
  console.log(params);
  // params 就是当前类
  params.prototype.apiUrl = "动态扩展的属性";
  params.prototype.run = function () {
    console.log("我是一个run方法");
  };
}

@logClass
class HttpClient {
  constructor() {}
  getData() {}
}
var http: any = new HttpClient();
console.log(http.apiUrl);
http.run();
  • 装饰器工厂(可传参)

target 就是类 params 就是参数

js
function logClass(params: string) {
  return function (target: any) {
    console.log(target);
    console.log(params);
    target.prototype.apiUrl = params;
  };
}

@logClass("http://www.loaderman.com/api")
class HttpClient {
  constructor() {}
  getData() {}
}

var http: any = new HttpClient();
console.log(http.apiUrl);

类装饰器重载构造函数

下面是一个重载构造函数的例子。 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

js

function logClass(target:any){
    console.log(target);
    return class extends target{
        apiUrl:any='我是修改后的数据';
        getData(){
            this.apiUrl=this.apiUrl+'----';
            console.log(this.apiUrl);
        }
    }
}


@logClass
class HttpClient{
    public apiUrl:string | undefined;
    constructor(){
        this.apiUrl='我是构造函数里面的apiUrl';
    }
    getData(){
        console.log(this.apiUrl);
    }
}

var http=new HttpClient();
http.getData();

// 我是修改后的数据----

属性装饰器

属性装饰器表达式会在运行时当作函数被调用,传入下列 2 个参数: 1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 2、成员的名字。

js

//类装饰器
function logClass(params: string) {
  return function (target: any) {
    console.log(target);
    console.log(params);
  };
}

//属性装饰器

function logProperty(params: any) {
  return function (target: any, attr: any) {
    console.log(target);
    console.log(attr);
    target[attr] = params;
  };
}
@logClass("xxxx")
class HttpClient {
  @logProperty("http://loaderman.com")
  public url: any | undefined;
  @logProperty("http://loaderman333.com")
  public urldata: any | undefined;
  constructor() {}
  getData() {
    console.log(this.url);
    console.log(this.urldata);
  }
}
var http = new HttpClient();
http.getData();

方法装饰器

  • 第一种(常用)

它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰会在运行时传入下列 3 个参数: 1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 2、成员的名字。 3、成员的属性描述符。

注意:在 vscode 编辑时有时会报作为表达式调用时,无法解析方法修饰器的签名。错误,此时需要在 tsconfig.json 中增加 target 配置项:

js
{
    "compilerOptions": {
        "target": "es6",
        "experimentalDecorators": true,
    }
}
js


function get(params :any){
    console.log(params);
    return function(target: any,methodName : any,desc:any){
      console.log(target);
      console.log(methodName);
      console.log(desc);

      target.apiUrl="xxxx" ;
      target.run=function(){
        console.log( "run" );
      }
    }
  }
  class Httpclient{
    public url:any |undefined;
    constructor(){

    }
    @get("http://www.itying.com")
    getData(){
        console.log(this.url);
    }
}

  var http: any = new Httpclient()
  console.log(http.apiUrl);
  http.run();
  • 方法装饰器二

在方法装饰器里面修改当前方法,把当前方法里面的参数修改成字符串

js
function get(params :any){
    console.log(params);
    return function(target: any,methodName : any,desc:any){
      console.log(target);
      console.log(methodName);
      console.log(desc.value);

      //修改装饰器的方法﹑把装饰器方法里面传入的所有参数改为string类型
      //1、保存当前的方法
      var oMethod = desc.value;
      desc.value = function(...args:any[]){
        args = args.map((value) => {
          return String(value)
        })

        console.log(args);
        oMethod.apply(this,args)
      }

    }
  }
  class Httpclient{
    public url:any |undefined;
    constructor(){

    }
    @get("http://www.itying.com")
    getData(...args:any[]){
        console.log("我是getData里面的方法",args);
    }
}

var http = new Httpclient();
http.getData(123,'xxx')
  • 方法装饰器三

参数装饰器表达式会在运行时当作函数被调用,传入下列 3 个参数: 1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 2、方法的名字。 3、参数在函数参数列表中的索引。

js
function logParams(params :any){
  console.log(params);
  return function(target: any, methodsName : any, paramsIndex:any){
    console.log(target);
    console.log(methodsName);
    console.log(paramsIndex);
  }
}

class Httpclient{
  public url:any |undefined;
  constructor(){

  }
  getData(@logParams("uuid") uuid: any){
      console.log("我是getData里面的方法");
      console.log(uuid);

  }
}

var http = new Httpclient();
http.getData(123456)

装饰器加载顺序

js
function ClassDecorator() {
  return function (target) {
    console.log("I am class decorator");
  };
}
function MethodDecorator() {
  return function (target, methodName: string, descriptor: PropertyDescriptor) {
    console.log("I am method decorator");
  };
}
function Param1Decorator() {
  return function (target, methodName: string, paramIndex: number) {
    console.log("I am parameter1 decorator");
  };
}
function Param2Decorator() {
  return function (target, methodName: string, paramIndex: number) {
    console.log("I am parameter2 decorator");
  };
}
function PropertyDecorator() {
  return function (target, propertyName: string) {
    console.log("I am property decorator");
  };
}

@ClassDecorator()
class Hello {
  @PropertyDecorator()
  greeting: string;
  @MethodDecorator()
  greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) {}
}

输出结果

bash
I am property decorator
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am class decorator

从上述例子得出如下结论: 1、属性装饰器 》方法装饰器》方法参数 装饰器》类装饰器

2、如果有多个同样的装饰器时:从最后一个装饰器依次向前执行

3、方法装饰器和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。上述例子中属性和方法调换位置,输出如下结果:

bash

I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am property decorator
I am class decorator