目 录CONTENT

文章目录

TS(八) 装饰器

Typescript(八) 装饰器

个人理解

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

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

基础

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

function setProp(target) {
  // todo
}

@setProp
class Person {}

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

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

装饰器工厂(传值)

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

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

@setProp()
class Person {}

装饰器组合

装饰器可以组合 也就是同一个目标可以有多个装饰器

@setProp()
@setProp()
class Person {}

装饰器执行顺序

  • 装饰器工厂从上到下依次执行,但是只是用于返回函数 但不调用函数
  • 装饰器函数由内到外执行,也就是执行工厂函数的返回函数,越靠近目标的装饰器,里面的函数越先执行。
  • 举例
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'
*/

装饰器求值

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

  • 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个实例成员;
  • 参数装饰器,方法装饰器,访问符装饰器或属性装饰器应用到每个静态成员;
  • 参数装饰器应用到构造函数;
  • 类装饰器应用到类。

类装饰器

  • 类装饰器在类声明之前声明,要记着装饰器要紧挨着要修饰的内容,类装饰器应用于类的声明。
  • 类装饰器表达式会在运行时当做函数被调用,它由唯一一个参数,就是装饰的这个类。
let sign = null;
function setName(name: string) {
  return function (target: Function) {
    sign = target;
    target.name = name;
  };
}
@setName("lison") // Info
class Info {
  constructor() {}
}
console.log(Info.name); // lison
console.log(sign === Info); // true
console.log(sign === Info.prototype.constructor); // true

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

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

  • 举例
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 就是参数

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);

类装饰器重载构造函数

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

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、成员的名字。

//类装饰器
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 配置项:

{
    "compilerOptions": {
        "target": "es6",
        "experimentalDecorators": true,
    }
}
  • 代码如下
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();

方法装饰器二

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

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、参数在函数参数列表中的索引。
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);

装饰器加载顺序

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) {}
}
  • 输出结果
I am property decorator
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am class decorator

总结

从上述例子得出如下结论:

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

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

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

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

评论区