前言

之前了解过一些,但是因为没有做专门的笔记,最近学习nest.js的时候,到处都是装饰器,不得已重温了一下ts的装饰器,并弄个笔记记录一下。

typescript提供了几种装饰器:

  1. 类装饰器
  2. 方法装饰器
  3. 属性装饰器
  4. 访问器装饰器
  5. 参数装饰器

在使用装饰器之前,我们还需要配置一下tsconfig.json,开启两个配置:

{
  "compilerOptions": {
    "target": "ESNext",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
  }
}

其中target不能低于es5版本,因为装饰器大量用到了描述符对象,而这个对象是在es5版本提供的,不过现在es5标配了。

emitDecoratorMetadata表示开启元数据支持,同时还需要安装一个依赖:

pnpm i reflect-metadata

安装完成后在你的代码入口处或者需要的地方import引入:

import "reflect-metadata";

这个的东西用于给参数访问器使用的,因为参数访问器是无法拿到具体的参数的,它只有参数的位置,那么为了能够去改变参数或者一些其他操作,往往都是先将参数的下标存到数组中,这个数组存在一个元数据对象中,这个元数据对象可以挂载在当前类或者其他地方。

然后配合类装饰器或者方法装饰器使用,在这些装饰器中我们才可以控制到传入的参数。

experimentalDecorators表示允许使用装饰器。

装饰器

类装饰器

这个非常好理解,我们的class其实就是构造函数的语法糖,那么类装饰器就是函数套函数的一种方式。

/** 类装饰器 */
const classDecorator: ClassDecorator = (target) => {
  target.prototype.name = "我是通过类装饰器添加的";
};

@classDecorator
class Test {
  constructor() {}
}

const test = new Test();
console.log(test.name);  //我是通过类装饰器添加的

target就是Test,它作为了参数传给了函数classDecorator,class是语法糖,Test就是构造函数,所以我们还是可以通过原型的方式进行扩展操作。

但是这里会有一个问题,就是类型的推断:

可以看到新增的属性是无法被准确推断出来的,哪怕我们通过类型重载的方式:

/** 类装饰器 */
const classDecorator = <T extends { new (...args: any[]): {} }>(target: T) => {
  // target.prototype.name = "我是通过类装饰器添加的";
  return class extends target {
    public name = "我是通过类装饰器添加的";
  };
};

@classDecorator
class Test {
  constructor() {}
}

const test = new Test();
console.log(test.name);

由此可见,其实类装饰器不太适合扩展,它更适合调整默认值,属性或者方法的替换,但是需要保证返回值的一致,因为类型推断出来的还是原来class的结果。

在nest.js里面,类装饰被用来做初始化参数传递了,它有一个@Injectable()类装饰器,还有@Inject参数装饰器,参数装饰器通过元数据将需要的参数记录下来,然后在通过类装饰器在运行时在共享的对象中获取Inject指定的对象。

然后将参数传入并实例化。

方法装饰器

方法装饰器会在方法声明后运行,具体看个例子:

/** 方法装饰器 */
const methodDecorator: MethodDecorator = (target, propertyKey, descriptor) => {
  console.log(target, propertyKey, descriptor);
};

class Test {
  constructor() {}

  @methodDecorator
  public show() {
    console.log("Hello World");
  }
}

const test = new Test();
test.show();

// {constructor: ƒ, show: ƒ} 'show' {writable: true, enumerable: false, configurable: true, value: ƒ}
// Hello World

因为我们如果要改动一个东西,肯定要在运行前就改好。

方法装饰器接收三个参数:

  1. target 如果是静态方法,它就是构造函数本身,反之则是类的原型对象
  2. propertyKey 成员的名字,也就是方法名
  3. descriptor 描述符对象

我们如果要改动一个方法,可以修改描述对象value属性,将value替换成新的方法已实现新的功能。

/** 方法装饰器 */
const methodDecorator: MethodDecorator = (target, propertyKey, descriptor: PropertyDescriptor) => {
  descriptor.value = function () {
    console.log("hello decorator");
  };
};

class Test {
  constructor() {}

  @methodDecorator
  public show() {
    console.log("Hello World");
  }
}

const test = new Test();
test.show(); // hello decorator

属性装饰器

和方法装饰器差不多,也是在声明后运行,但是它只有两个参数:

  1. target 如果是静态属性,它就是构造函数本身,反之则是类的原型对象
  2. propertyKey 属性的名称
/** 属性装饰器 */
const propertyDecorator: PropertyDecorator = (target, propertyKey) => {
  console.log(target, propertyKey);
};

class Test {
  @propertyDecorator
  public name: string = "test";

  constructor() {}
}

const test = new Test();
// {constructor: ƒ} 'name'

我们可能会这么修改:

/** 属性装饰器 */
const propertyDecorator: PropertyDecorator = (target: any, propertyKey) => {
  console.log(target, propertyKey);
  target[propertyKey] = "decorator";
};

class Test {
  @propertyDecorator
  public name: string = "test";

  constructor() {}
}

const test = new Test();
console.log(test);   // test

实际上还是test,我们修改的其实是原型,因为public name: string = "test"转换为的其实是this.name = "test",这是实例的属性,装饰器是没法修改的。

可以看到我们的东西确实是在原型上的。

这也间接说明,属性装饰器的应用场景是很小的,官方定义属性装饰器用来监视类中是否声明了某个名字的属性。

所以了解一下即可。

访问器装饰器

访问器装饰器作用于访问器上,说人话就是get\set上,但是不允许同一个属性get、set都使用访问器装饰器,你只能选择一个,因为它是通过描述符对象进行修改的,这个对象上可以直接改get、set方法,它内部整合了,不需要再单独声明。

它接收三个参数:

  1. target 静态成员是构造函数,反之则是原型对象
  2. propertyKey 属性的名称
  3. descriptor 描述符对象
/** 访问器装饰器 */
const accessorDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  let value = "default";
  descriptor.get = () => {
    return value;
  };
  descriptor.set = (newValue: string) => {
    value = newValue;
  };
};

class Test {
  private _name: string = "test";

  @accessorDecorator
  get name() {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }

  constructor() {}
}

const test = new Test();
console.log(test.name);
test.name = "new name";
console.log(test.name);

这个比属性访问器稍微强一些,但是还是要注意,实例化后的对象属性,如果不是原型上的我们是改不了的。

参数装饰器

参数装饰器有三个参数:

  1. target 静态成员是构造函数,反之则是原型对象
  2. propertyKey 属性的名称
  3. parameterIndex 参数的下标
/** 参数装饰器 */
const parameterDecorator: ParameterDecorator = (target, propertyKey, parameterIndex) => {
  console.log(target, propertyKey, parameterIndex);
};

class Test {
  constructor(@parameterDecorator private name: string) {}
}

const test = new Test("test");
// class { constructor(name) { this.name = name; } } undefined 0

可以看到,有效的就是第一个和第三个参数,而在nest.js中,装饰器会接收一个参数,这个参数可以是string,也可以是具体的类,这个参数会被存起来,作为元数据存在某处,然后通过Injectable类装饰器将参数进行注入,所以,它的参数如果要传只能是通过@Inject装饰器进行注入,否则就会报错。

因为类装饰器只能拿到被装饰的类,而不能获取到类实例化时的...args参数数组,所以,为了防止漏参数,只能报错,必须根据预设规则传参。

具体的做法我贴一个ts的官方例子吧:

import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
 
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
 
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
  let method = descriptor.value!;
 
  descriptor.value = function () {
    let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  };
}

装饰器接参

装饰器都是可以接参数的,无非就是多套一层函数就是了。

这里举个参数装饰器的例子:

/** 参数装饰器 */
const parameterDecorator = (type: Number): ParameterDecorator => {
  return (target, propertyKey, parameterIndex) => {
    console.log(type, target, propertyKey, parameterIndex);
  };
};

class Test {
  constructor(@parameterDecorator(1) private name: string) {}
}

const test = new Test("test");
// 1 class { constructor(name) { this.name = name; } } undefined 0
分类: TypeScript 标签: 装饰器Decorators类装饰器方法装饰器属性装饰器访问器装饰器参数装饰器

评论

暂无评论数据

暂无评论数据

目录