TS三-装饰器和类

官方文档地址:装饰器 TS演练场

装饰器

目前(时间:2023-7-4;TS版本:5.1.6)装饰器(decorator)的特性还未正式成为js的规范(以下内容均是stage2语法),所以即便现在ts里面可以使用,但是还是作为实验性特性的功能 ,在未来的版本中可能会发生改变.

要启用对装饰器的实验性支持,你必须在命令行或tsconfig.json中启用experimentalDecorators编译器选项:

# Command Line:
tsc --target ES5 --experimentalDecorators
// tsconfig.json:
{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

写法

装饰器Decorator)是一种特殊的声明,可用于修改类、方法、属性或参数的行为。装饰器通过在被装饰元素的声明之前使用 @ 符号来使用。可以多次应用装饰器,并且它们按照装饰器列表的顺序应用。

普通装饰器

直接将函数作为装饰器函数:

function ClassDecorator(target: any) {
  // target ...
  console.log(target)
}

@ClassDecorator
class TestClass {
  constructor() { }
}

装饰器工厂函数

装饰器工厂函数,在调用时可以传递参数:

// 利用函数柯里化解决传参问题, 向装饰器传入一些参数,也可以叫 参数注解
function ClassDecorator(info: string) {
  return (target: any) => {
    //  ...
  }
}

@ClassDecorator('test')
class TestClass {
  constructor() { }
}

类装饰器 Class Decorators

类装饰器:放置在类定义之前,作用于类声明上,有且仅有一个参数。该参数指的是类的构造函数(也可以说是类本身,因为ES6 class的构造函数本来就等于 class 自身…)

class Test {
  constructor() {
  }
}
console.log(new Test().constructor === Test); // true

如果类装饰器的函数有返回值(新的构造函数),那么它将用返回的构造函数 来替换原有的 类的声明。

TS 内部声明的类装饰器的类型ClassDecorator:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

示例1:防止类的实例被修改

const sealed:ClassDecorator = (constructor: Function) => {
  // Object.seal()密封一个对象等价于阻止其扩展,可以改变属性的值,但无法修改或删除属性。
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("World!");
console.log(greeter.greet())  // OK: Hello, World!

Greeter.prototype.test = "Test";   // Error

示例2:用于添加一个静态方法和修改类的属性

<T extends { new (...args: any[]): any }>定义一个泛型类型参数T,该参数必须是具有构造函数类型的对象。其构造函数必须接受任意数量的参数并且返回一个对象(可以是任何类型)。

/**
 * {new (...args: any[]): any } 可以改为 { new (...args: any[]): TestClass } 
 */
const TestDeco = <T extends { new (...args: any[]): any }>(target: T) => {
  // return class extends target {  // 匿名类
  return class Test extends target {  // 类名Test

    static addr:string = 'aaddr'

    constructor(...args: any[]) {
      super(...args)
      // console.log('子类', this.name, Test.addr) // 用 `static` 定义的属性和方法,不可以通过 `this` 去访问,只能通过类名去调用
    }

    // 新增静态方法
    newMethod() {
      console.log('子类 newMethod', this.name, Test.addr);
    }
    // 修改类属性
    sayAddr(){
      console.log('修改后sayAddr log...');
    }
  }
}

// TestDeco2 这是和 TestDeco使用继承 不同的第二种写法
const TestDeco2 = (name: string): ClassDecorator  => {
  return (target: Function) => {
    target.prototype.getNames = ():string => {
      return name
    }
  }
}

@TestDeco
@TestDeco2('nameTest')
class TestClass {
  static oldProp: string = 'OldProp'

  constructor(public name: string) {  }

  sayAddr() {
    console.log('sayAddr', this.name, TestClass.oldProp);
  }
  method() {
    console.log('父类 method log...');
  }
}

const test = new TestClass('Join')
test.sayAddr();  // 添加TestDeco前log:sayAddr Join OldProp; 添加后log:修改后sayAddr log...
test.method();  // 父类 method log...
(test as any).newMethod();  // 子类 newMethod Join aaddr
console.log((test as any).getNames());  // nameTest

在装饰器函数 TestDeco 中,<T extends { new (...args: any[]): any }>(target: T) => {...} 接受一个名为 target 的类构造函数对象,并返回一个新的类定义,该类定义必须继承 target 并具有新的扩展。

方法装饰器 Method Decorators

方法装饰器是在方法声明之前声明的。装饰器应用于方法的属性描述符,可以用于观察、修改或替换方法的定义。方法装饰器不能在声明文件、重载或任何其他环境上下文中使用(比如在声明类中)。

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

    • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    • 成员的名称。
    • 成员的属性描述符。(如果脚本目标小于 ES5,属性描述符将为 undefined
  • 如果方法装饰器返回一个值,它将被用作方法的属性描述符。(如果脚本目标小于 ES5,则会忽略返回值)

  • TS 内部声明的类装饰器的类型MethodDecorator:

    declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
    
function testMethodDecorator(info: string): MethodDecorator {
  return (target: any, methodName: any, descriptor: PropertyDescriptor) => {
    console.log('1', target)      // {constructor: ƒ, getTitle: ƒ}
    console.log('2', methodName)  // getTitle
    console.log('3', descriptor)  // {writable: true, enumerable: false, configurable: true, value: ƒ getTitle()}
  }
}

class Demo {
  title: string
  constructor(msg: string) {
    this.title = msg
  }

  @testMethodDecorator('TestDecorator')
  getTitle() { }
}

示例1:使用方法装饰器来记录类的方法调用次数

function methodCount(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    methodCountMap[key] = methodCountMap[key] ? methodCountMap[key] + 1 : 1;

    // 调用原始方法
    const result = originalMethod.apply(this, args)
    return result
  }

  return descriptor;
}

const methodCountMap: { [key: string]: number } = {}

class UserService {
  private userList: string[] = []
  constructor() { }

  @methodCount
  addUser(name: string) {
    console.log('Adding user:', name);
    this.userList.push(name)
    console.log('用户添加成功!');
  }

  @methodCount
  getUserList() {
    console.log('User list:', this.userList);
  }
}

const userService = new UserService();

userService.addUser('Join');  // 输出:Adding user: Join   // 用户添加成功!
userService.addUser('Jack');  // 输出:Adding user: Jack   // 用户添加成功!

userService.getUserList();    // User list: (2) ['Join', 'Jack']

console.log('methodCountMap', methodCountMap);  // {addUser: 2, getUserList: 1}

示例2: 实现一个GET请求

import axios from 'axios';

const Get = (url: string): MethodDecorator => {
  return (target, key, descriptor: PropertyDescriptor) => {
    const fnc = descriptor.value;
    axios.get(url).then(res => {
      fnc(res, {
        status: 200,
      })
    }).catch(e => {
      fnc(e, {
        status: 500,
      })
    })
  }
}

//定义控制器
class Controller {
  constructor() {

  }
  @Get('https://api.apiopen.top/api/getHaoKanVideo?page=0&size=10')
  getList(res: any, status: any) {
    console.log(res.data.result.list, status)
  }

}

扩展:使用targetdescriptor的区别

  1. 使用 descriptor 可以对方法的属性描述符修改。属性描述符定义了方法的特性和行为,用于修改或定义类方法的属性(包括属性的可访问性(writable)、属性的值(value)、属性是否可枚举(enumerable)还有get / set等等)。
  • 例如示例1代码,在addUsergetUserList方法上应用了methodCount方法装饰器。descriptor对象的修改允许我们在这两个方法被调用时,记录日志,且不会修改原始方法
  1. 使用 target 可以直接在原型对象上修改方法的函数体。不推荐使用原因:可能会覆盖原始方法的实现,并影响程序的正常运行。
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = target[key];

  target[key] = function (...args: any[]) {
    const result = originalMethod.apply(this, args)
    return result
  }
}

class UserService {
  constructor(private users: string[] = []) {}

  @Log
  public getUserList(): string[] {
    return this.users;
  }

  @Log
  public addUser(user: string) {
    this.users.push(user);
  }
}

const userService = new UserService(['Alice', 'Bob']);
console.log(userService.getUserList()); // ["Alice", "Bob"]
userService.addUser('Charlie');
console.log(userService.getUserList()); // ["Alice", "Bob", "Charlie"]

在上述代码中,使用 target 直接替换了 getUserListaddUser 方法,并将其覆盖到一个新的函数体。这允许我们在这些方法被调用时记录日志。注意,使用 target 直接替换函数体并不建议使用,因为它可能会覆盖原始方法的实现,并影响程序的正常运行。

为何使用originalMethod.apply(this, args)

    1. 通过使用apply,可以确保在调用原始方法时保持正确的定义的上下文和参数
    1. 若不使用它来调用原始方法,而是直接使用originalMethod(),则会导致以下问题:
    • 上下文丢失:如果不使用apply而直接使用originalMethod(),可能会导致原始方法内部的this关键字丢失对上下文的引用。使用apply可以确保引用正确的上下文(即类的实例)。

    • 参数传递不正确:如果原始方法带参数,并且我们不使用apply方法传递参数,那么调用originalMethod()时将无法正确传递参数。使用apply才可以正确传递。

不使用apply示例代码:

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    // const result = originalMethod.apply(this, args)
    const result = originalMethod(args[0], args[1], args[2]);
    return result;
  }

  return descriptor;
}

class UserService {
  constructor(private users: string[] = []) { }

  @log
  public getUserList(): string[] {
    return this.users;
  }

  @log
  public addUser(user: string) {
    this.users.push(user);
  }
}

const userService = new UserService(['Alice', 'Bob']);
console.log(12, userService.getUserList()); // ["Alice", "Bob"]
userService.addUser('Charlie');
console.log(13, userService.getUserList()); // ["Alice", "Bob", "Charlie"]

在上述代码中,我们在 log 方法装饰器中不使用 apply,而是手动传递所有方法参数。这种方法可能会有以下问题:

  1. 参数个数和顺序要保持一致

由于方法的参数的数量和顺序可能会改变,当使用一个硬编码的方式调用函数时,必须按照固定的参数数量和顺序来调用函数。如果代码中使用了 rest 参数语法,或者在调用方法的时候修改了方法的参数个数和顺序,则这种方法可能会带来一些麻烦和错误。

  1. 难以维护

当被修饰的方法的参数列表变更时,除了在方法体中手动调整方法调用时的参数,还需要在装饰器中也要相应做出改变。这会导致代码更加难以维护和扩展。

rest 参数语法一种 ES6 新增的语法,它允许函数接受不定数量的参数,并将它们转换为一个数组,语法格式如下:

function functionName(...args: argType[]) {
  // function body
}

属性装饰器 Property Decorators

属性装饰器在属性声明之前声明,属性装饰器表达式会在运行时当做函数被调用。带有以下两个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

  2. 成员的名称。

注意:由于 TypeScript 中原型成员定义时没有一种描述实例属性的机制,也没有一种观察或修改属性初始化器的方法,因此属性装饰器不能将 属性描述符 作为参数提供。此外,返回值也会被忽略。因此,属性装饰器只能用于观察已为类声明了特定名称的属性。

TS 内部声明的类装饰器的类型PropertyDecorator:

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

示例1: 格式化时间戳

import 'reflect-metadata';
import dayjs from 'dayjs';

// 时间戳转日期格式装饰器
function DateFormat(format: string = 'YYYY-MM-DD HH:mm:ss') {
  return (target: any, propertyKey: string) => {
    let val = Reflect.get(target, propertyKey);
    
    Reflect.set(target, propertyKey, {
      get: () => {
        return dayjs.unix(val).format(format);
      },
      set: (value: any) => {
        val = value;
      },
      enumerable: true,
      configurable: true
    });
  }
}

class MyClass {
  @DateFormat('YYYY-MM-DD')
  public timer!: number;
}

let myClass = new MyClass();
myClass.timer = Date.now() / 1000; // 注意,dayjs的unix方法需要的是秒,而Date.now()返回的是毫秒,所以这里要进行转换
console.log(myClass.timer); // 打印当前日期 YYYY-MM-DD

不使用js库:

// 时间戳转日期格式装饰器
function DateFormat(format: string = 'yyyy-mm-dd') {
  return function DateFormatDecorator(target: any, propertyKey: string) {
    let value = target[propertyKey];
    const getter = () => {
      const date = new Date(value);
      return date.getFullYear()
        + '-' + ('0' + (date.getMonth() + 1)).slice(-2)
        + '-' + ('0' + date.getDate()).slice(-2)
        + ' ' + ('0' + date.getHours()).slice(-2)
        + ':' + ('0' + date.getMinutes()).slice(-2)
    };

    const setter = (newVal: number) => {
      value = newVal;
    };

    if (delete target[propertyKey]) {
      Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter,
        enumerable: true,
        configurable: true
      });
    }
  }
}

class MyClass {
  @DateFormat('yyyy-mm-dd') public timer!: number;
}

let myClass = new MyClass();
myClass.timer = Date.now()
console.log(myClass.timer); // 打印当前日期 yyyy-mm-dd hh:mm

示例1: 判断属性值是否符合范围

function ageRange(min: number, max: number) {
  return (target: any, propertyKey: string): void => {
    let value = target[propertyKey]

    const getter = (): any => value
    const setter = (next: number): void => {
      if (next < min || next > max) {
        // throw new Error(`age应该在 ${min} 到 ${max} 的范围内`);
        console.log(`age应该在 ${min}${max} 的范围内`)
        value = null
      }else{
        value = next;
      }
    }
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    })
  }
}

class ExampleClass {
  @ageRange(18, 35)
  public age: number
  constructor(age: number){
    this.age = age
  }
}

const userAge = new ExampleClass(20)
console.log(userAge.age);   // 20

userAge.age = 40
console.log(userAge.age);   // null   // "age应该在 18 到 35 的范围内"

参数装饰器 Parameter Decorators

参数装饰器是定义在方法参数上的装饰器;能在方法,构造函数,或者方法的 setter/getter 上使用;参数装饰器可以被用于扩展或修改函数、类或者方法中传入的参数的行为。

当一个参数装饰器被应用到参数时,它将被调用时间是在运行时,而且带有以下三个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. 成员的名称。
  3. 参数在函数参数列表中的索引。

注意,参数装饰器没有返回值。

TS 内部声明的类装饰器的类型ParameterDecorator:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

基础示例

function logParameter(target: any, proKey: string, parameterIndex: number){
  console.log(target);    // {constructor: ƒ, print: ƒ}
  console.log(proKey);    // print
  console.log(parameterIndex);  // 0
}

class MyClass{
  print(@logParameter name: string){
    console.log('PrintLog:', name);
  }
}

let test = new MyClass()
test.print('7/21')

检查参数是否为空

参数装饰器主要用于注入元数据。但可以借助方法装饰器来辅助实现参数检查的功能。

// 导入`reflect-metadata` 包来启用装饰器的元编程能力。这个库增加了在`JavaScript`中获取类型信息的能力。
import 'reflect-metadata';

// 定义一个全局唯一的Symbol作为元数据的键。
const requiredMetadataKey = Symbol("required"); 

// validate 则用来在调用方法时读取元数据,并检查必要的参数是否为空。若为空,则抛出错误。否则,调用原方法。
function validate(target: any, propertyKey: any, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: string[]) {

    let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey)

    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (parameterIndex >= arguments.length || arguments[parameterIndex] === null || arguments[parameterIndex] === undefined) {
          throw new Error("'参数不能为空");
        }
      }
      return originalMethod.apply(this, args)
    }
  }
}

// required 用来标记需要检查非空的参数,并将这些参数的索引存储在元数据中;
function required(target: any, propertyKey: string, paramIdx: number) {
  let existingRequiredParameter: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameter.push(paramIdx)

  Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameter, target, propertyKey)
}


class MyClass {
  @validate
  greet(@required name: string) {
    console.log('GreetLog:', name);
  }
}
const obj = new MyClass();
obj.greet('John');  // 输出: GreetLog: John
obj.greet(null);    // 抛出错误: Uncaught (in promise) Error: '参数不能为空

greet 方法被调用时,validate 方法装饰器将执行,它读取元数据,检查哪些参数被 required 标记为需要检查。然后,validate 检查这些参数是否为空,如果为空,将抛出错误。如果所有的参数都不为空,那么将正常执行原始的方法。

访问符装饰器 Accessor Decorators

访问符装饰器 应用于存取器的声明,也就是 getter/setter 上。

访问符装饰器的类型和语法基本与 方法装饰器 一致

访问符装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

访问符装饰器可以返回一个属性描述符用来替换原来的描述符。

  • 如果访问符装饰器返回一个新的描述符,系统将使用该描述符来修改类的属性。
  • 如果访问符装饰器没有返回值,系统将使用原有的描述符。

注意:不能向多个同名的 get/set 访问器应用修饰器

示例

一般使用场景:只想在getter / setter时机做一些事情

class Point {
  private _x: number;
  private _y: number;

  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

  @configurable(false)
  get x() {
    return this._x;
  }

  @configurable(false)
  get y() {
    return this._y;
  }
}

function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = value;  // 配置类的属性访问器的configurable值,使得它们的属性描述符不可被修改。
  };
}

装饰器执行顺序

  • 属性方法先执行,谁先写 先执行谁
  • 方法的时候, 先参数再方法,而且一定会在一起
  • 最后是类
  • 如果同类型,先执行离类近的
function classDecorator(target: any) {
  console.log('类装饰器-执行');
}


function paramterDecorator(target: any, propertyKey: any, parameterIndex: number) {
  console.log('(构造器中的)参数装饰器-执行');
}
function firstParamterDecorator(target: any, propertyKey: string, parameterIndex: number) {
  console.log('第一个(方法的)参数装饰器-执行');
}
function secondParamterDecorator(target: any, propertyKey: string, parameterIndex: number) {
  console.log('第二个(方法的)参数装饰器-执行');
}


function properDecorator(target: any, propertyKey: string) {
  console.log('属性装饰器-执行');
}


function firstMethodDecorator(target: any, methodName: any, descriptor: PropertyDescriptor) {
  console.log('第一个方法装饰器-执行');
}
function secondMethodDecorator(target: any, methodName: any, descriptor: PropertyDescriptor) {
  console.log('第二个方法装饰器-执行');
}


@classDecorator
class Test {
  constructor(@paramterDecorator age: number) { }

  @firstMethodDecorator
  say(@firstParamterDecorator pname: string) { }

  @properDecorator
  public name: string = 'Jack'

  @secondMethodDecorator
  print(@secondParamterDecorator addr: string) { }
}
const test = new Test(20)
test.say('sayName')
test.print('printAddr')

/**
 * 执行结果:
 *  第一个(方法的)参数装饰器-执行
 *  第一个方法装饰器-执行
 *  属性装饰器-执行
 *  第二个(方法的)参数装饰器-执行
 *  第二个方法装饰器-执行
 *  (构造器中的)参数装饰器-执行
 *  类装饰器-执行
 */

由执行结果可以看出在定义的应用函数装饰器中,方法参数装饰器执行的优先级最高,而方法和属性装饰器受到定义顺序的影响。类与构造器参数装饰器优先级最低。

扩展:reflect-metadata

Reflect MetadataES7 的一个提案,它主要用来在声明的时候添加和读取元数据

reflect-metadata是一个用于 TypeScript 的库,它提供了一组用于元数据(Metadata)的API元数据是关于代码结构、类型和逻辑的描述性信息,可以用于运行时的反射元编程,帮助我们在 TypeScript 中对类、成员属性和方法等进行注解和装饰

安装:npm install reflect-metadata —save

要使用它,首先要安装它的依赖项,并在 TypeScript 的配置文件中启用 experimentalDecoratorsemitDecoratorMetadata 选项,以支持装饰器和元数据的使用。

以下是这个库的index.d.ts,可以看出该库没有任何导出, 再往下是对global和命名空间的声明, 于是我们在项目中直接使用import 'reflect-metadata'引入后便可在项目中访问到.

export { };

declare global {
  namespace Reflect { }
}

Reflect.metadata(metadataKey, metadataValue)

  • 作用:用于为类的属性或方法添加元数据

    // 参数类型:
    function metadata(metadataKey: any, metadataValue: any): {
      (target: Function): void;
      (target: Object, propertyKey: string | symbol): void;
    };
    
    /**
     * @param metadataKey: any 元数据的键(可以是任何有效的JavaScript值)
     * @param metadataValue: any 元数据的值(可以是任何有效的JavaScript值)
     * @returns (装饰器函数)没有显式的返回值,它会将元数据关联到相应的属性或方法。
     */
    
  • 示例:

    // 定义元数据
    const MyMetadataKey = Symbol('myMetadata');
    
    // 1.类元数据
    @Reflect.metadata('K1class', 'classMetaVal')
    class MyClass {
    
      // 2.属性元数据
      @Reflect.metadata(MyMetadataKey, 'Hello, metadata!')
      public myProperty: string = 'test';
    
      // 3.方法元数据
      @Reflect.metadata('K3Method', 'Metadata for myMethod')
      myMethod() {
        console.log('myMethod');
      }
    
      // 4.静态方法元数据
      @Reflect.metadata('K4static', 'sayHello')
      static sayHello() {
        console.log('hello static');
      }
    }
    
    const classVal = Reflect.getMetadata('K1class', MyClass)
    console.log(11, classVal);  // 11 'classMetaVal'
    
    const test = new MyClass()
    
    const propVal = Reflect.getMetadata(MyMetadataKey, test, 'myProperty')
    console.log(22, propVal);  // 22 'Hello, metadata!'
    
    const methodVal = Reflect.getMetadata('K3Method', test, 'myMethod')
    console.log(33, methodVal);  // 33 'Metadata for myMethod'
    
    const staticVal = Reflect.getMetadata('K4static', MyClass, 'sayHello')
    console.log(44, staticVal);  // 44 'sayHello'
    

Reflect.defineMetadata(metadataKey, metadataValue, target [, propertyKey])

  • 作用:用于在给定的目标(target)或目标的特定属性上定义元数据。

    function defineMetadata(metadataKey: any, metadataValue: any, target: Object, propertyKey?: string | symbol): void;
    /**
     * @param target:表示要定义元数据的目标(类或对象)。
     * @param propertyKey(可选):类的属性名或方法名,用于定义特定成员的元数据。
     */
    
  • 示例

    import 'reflect-metadata';
    // 定义元数据
    const myMeta = Symbol('myMetadata');
    
    class MyClass {
      static staticProperty: string = 'test';
      myMethod() {
        console.log('myMethod');
      }
      static sayHello() {
        console.log('hello static');
      }
    }
    
    // 装饰
    Reflect.defineMetadata(myMeta, 'classAAA', MyClass)
    const clasMeta = Reflect.getMetadata(myMeta, MyClass)   // log: 'classAAA'
    
    // Reflect.defineMetadata(myMeta, 'staticSayHelloBBB', MyClass.sayHello)
    // const staticMethodMeta = Reflect.getMetadata(myMeta, MyClass.sayHello)   // log: 'staticSayHelloBBB'
    /**
    * 两种写法,不能混用;即通过 MyClass.sayHello 装饰的,也只能通过 MyClass.sayHello 取。
    * 写法二如下:
    */
    Reflect.defineMetadata(myMeta, 'staticSayHelloBBB', MyClass, 'sayHello')
    const staticMethodMeta = Reflect.getMetadata(myMeta, MyClass, 'sayHello')   // log: 'staticSayHelloBBB'
    
    Reflect.defineMetadata(myMeta, 'methodCCC', MyClass.prototype.myMethod)
    const methodMeta = Reflect.getMetadata(myMeta, MyClass.prototype.myMethod)   // log: 'methodCCC'
    
    Reflect.defineMetadata(myMeta, 'staticPropertyDDDD', MyClass, 'staticProperty')
    const staticPropMeta = Reflect.getMetadata(myMeta, MyClass, 'staticProperty')   // log: 'staticPropertyDDDD'
    
  • metadatadefineMetadata区别:

    • Reflect.defineMetadata是一个API方法,用于在特定的目标上定义元数据,通过手动调用API来实现。

    • Reflect.metadata是一个装饰器函数,可以直接应用于类或类成员,通过装饰器语法来定义元数据。

    • Reflect.metadata内部实际上调用了Reflect.defineMetadata方法来定义元数据。

    • 使用Reflect.defineMetadata直接定义元数据时,需要指定目标和(可选)属性;

    • 而使用Reflect.metadata装饰器时,装饰器会自动将所应用的目标和属性作为参数传递给Reflect.defineMetadata方法。

    在使用上的选择,可以根据具体情况来决定使用API方法还是装饰器函数,一般而言,使用装饰器语法更加简洁和直观,所以在可行的情况下推荐使用Reflect.metadata装饰器。

hasMetadata / hasOwnMetadata

  • 方法签名:

    • Reflect.hasMetadata(metadataKey, target [, propertyKey])

    • Reflect.hasOwnMetadata(metadataKey, target [, propertyKey])

    // hasMetadata
    function hasMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): boolean;
    // hasOwnMetadata
    function hasOwnMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): boolean;
    /**
     * @param metadataKey 表示要检查的元数据键。
     * @param target  表示要检查元数据的目标(类或对象)
     * @param propertyKey(可选):表示要检查元数据的目标的特定属性(仅适用于类成员的元数据)。
     * @returns 一个布尔值,表示元数据键是否存在于指定目标上。
     */
    
  • 作用和区别

    • hasMetadata:检查给定的元数据键(metadataKey)是否存在于指定的目标上。

    • hasOwnMetadata: 检查给定的元数据键(metadataKey)是否存在于指定的目标自身,即仅判断目标自身是否具有该元数据。(只查找对象上的元数据, 而不会继续向上查找原型链上的)

    import 'reflect-metadata';
    // 定义元数据
    const symbolMeta = Symbol('myMetadata');
    
    class MyClass {
      @Reflect.metadata(symbolMeta, 'myName')
      static myName: string = 'Join'
    }
    Reflect.defineMetadata(symbolMeta, 'Hello', MyClass);
    
    const t1 = Reflect.hasMetadata(symbolMeta, MyClass); // log: true
    const t2 = Reflect.hasMetadata(symbolMeta, MyClass, 'myName'); // log: true
    const t3 = Reflect.hasOwnMetadata(symbolMeta, MyClass, 'myName'); // log: true
    

getMetadata / getOwnMetadata

  • 方法签名:

    • Reflect.getMetadata(metadataKey, target [, propertyKey])

    • Reflect.getOwnMetadata(metadataKey, target [, propertyKey])

    // getMetadata
    function getMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): any;
    // getOwnMetadata
    function getOwnMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): any;
    /**
     * @param metadataKey 表示要检查的元数据键。
     * @param target  表示要检查元数据的目标(类或对象)
     * @param propertyKey(可选):表示要检查元数据的目标的特定属性(仅适用于类成员的元数据)。
     * @returns 表示指定元数据键的值,如果元数据不存在则返回undefined。
     */
    
  • 作用和区别

    • getMetadata: 从指定的目标上获取给定的元数据键(metadataKey)的值。

    • getOwnMetadata: 从指定的目标自身获取给定的元数据键(metadataKey)的值,即获取目标自身具有的元数据。(只查找对象上的元数据, 而不会继续向上查找原型链上的)

    class MyClass {
      @Reflect.metadata('key', 'value')
      myMethod() {
        // ...
      }
    }
    // const metadata = Reflect.getMetadata('key', MyClass.prototype, 'myMethod');
    const metadata = Reflect.getOwnMetadata('key', MyClass.prototype, 'myMethod');
    

getMetadataKeys / getOwnMetadataKeys

  • 方法签名:

    • Reflect.getMetadataKeys(target [, propertyKey])

    • Reflect.getOwnMetadataKeys(target [, propertyKey])

    // getMetadataKeys
    function getMetadataKeys(target: Object, propertyKey?: string | symbol): any[];
    // getOwnMetadataKeys
    function getOwnMetadataKeys(target: Object, propertyKey?: string | symbol): any[];
    /**
     * @param target  表示要检查元数据的目标(类或对象)
     * @param propertyKey (可选)用于获取指定属性的元数据键。在类的方法装饰器中使用时,可以指定具体的方法名称来获取该方法的元数据键。
     * @returns 返回类型是一个数组,包含元数据的键。
     */
    
  • 作用和区别

    • 这两个方法会返回指定对象 (target) 或其原型链上的所有元数据的键。可选的第二个参数 propertyKey 用于获取指定属性的元数据键。

    • 区别和别的...Own...方法一样:getOwnMetadataKeys只会对象自身的元数据,而不会检查原型链上的对象。

    class MyClass {
      @Reflect.metadata('key1', 'value1')
      @Reflect.metadata('key2', 'value2')
      myMethod() {
        // ...
      }
    }
    const metadataKeys = Reflect.getMetadataKeys(MyClass.prototype, 'myMethod');  // ["key1", "key2"]
    const ownMetadataKeys = Reflect.getOwnMetadataKeys(MyClass.prototype, 'myMethod');  // ["key1", "key2"]
    

Reflect.deleteMetadata(metadataKey, target, propertyKey)

  • 作用:删除对象中存储的指定元数据。

    function deleteMetadata(metadataKey: any, target: Object, propertyKey?: string | symbol): boolean;
    /**
     * @param metadataKey  要删除的元数据的键
     * @param target  目标对象,要删除元数据的对象。可以是类的原型对象或普通对象。
     * @param propertyKey (可选)用于删除指定属性的元数据。在类的方法装饰器中使用时,可以指定具体的方法名称来删除该方法的元数据。
     * @returns 返回类型是一个数组,包含元数据的键。
     */
    
  • 示例:

    import 'reflect-metadata';
    
    // 定义元数据
    const symbolMeta = Symbol('myMetadata');
    
    @Reflect.metadata(symbolMeta, 'value1')
    class MyClass {
      @Reflect.metadata(symbolMeta, 'value2')
      myMethod() {
        // ...
      }
    
      @Reflect.metadata(symbolMeta, 'value3')
      static staticPerty: string = 'join'
    }
    
    const t1 = Reflect.deleteMetadata(symbolMeta, MyClass)
    const t2 = Reflect.deleteMetadata(symbolMeta, MyClass.prototype, 'myMethod')
    const t3 = Reflect.deleteMetadata(symbolMeta, MyClass, 'staticPerty')
    console.log(t1, t2, t3);  // true true true
    

Reflect.decorate(decorators, target)

Reflect.decorate()装饰:接受一个装饰器的集合(Decorator[])给目标对象(可以是类、属性、方法)

  • 1.对类的装饰:Reflect.decorate(decorators: ClassDecorator[], target: Function),返回应用提供的装饰器后的值。(注意:装饰器应用是与array的位置方向相反, 为从右往左)

  • 2.对属性或方法装饰:Reflect.decorate(decorators:(PropertyDecorator | MethodDecorator)[], target: Object, targetKey: string | symbol, descriptor?:PropertyDescriptor): PropertyDescriptor

/**
 * 参数说明:
 * @decorators 一个装饰器数组,通过这个参数可以将多个装饰器应用到同一个目标上
 * @target 目标对象。目标对象可以是类的原型对象,用于装饰类的实例属性和方法;也是类本身,用于装饰静态成员;还可以是类的实例方法的上下文,用于装饰方法参数。
 * @propertyKey 要装饰的属性名称 key
 * @descriptor 被装饰的属性的描述 descriptor
 */

import 'reflect-metadata';

/******************* 分隔符 是示例一 ******************************************************/
const classDecorator: ClassDecorator = target => {
  target.prototype.sayName = () => console.log('装饰类');
}

// 应用对类的装饰
class MyClass {
  message: string;
  constructor(msg: string) {
    this.message = msg;
  }
  sayName() {
    console.log(this.message)
  }
}

Reflect.decorate([classDecorator], MyClass) // 对其进行装饰

const obj = new MyClass('Hello World');
// 调用装饰过的方法
obj.sayName();  // 装饰类


/******************* 分隔符 是示例二 ******************************************************/
// 属性装饰
const decoratePropertyDecorator = (target: any, propertyKey: string | symbol) => {
  const value = target[propertyKey]
  target[propertyKey] = () => {
    console.log('覆盖....')
    // value.call(target)
  }
}

// 方法装饰器
const decorateMethodDecorator = (target: any, propertyKey: string | symbol, descriptor: any) => {
  // 将其描述改为不可编辑
  descriptor.configurable = false
  descriptor.writable = false
  return descriptor
}

// 应用装饰器
class MyClass2 {
  static staticProperty() {
    console.log('MyClass2 staticProperty');
  };

  sayName() {
    console.log('MyClass2 sayName')
  }
}

/******* PropertyDecorator TEST *******/
Reflect.decorate([decoratePropertyDecorator], MyClass2, 'staticProperty')
// 测试装饰属性是否正确
MyClass2.staticProperty()   //覆盖....


/******* MethodDecorator TEST *******/
// 获取原descriptor:
let orginDescriptor = Object.getOwnPropertyDescriptor(MyClass2.prototype, 'sayName')
// 获取修改后的descriptor:
let updatedDescriptor = Reflect.decorate([decorateMethodDecorator], MyClass2, 'sayName', orginDescriptor)
// 将修改后的descriptor添加到对应的方法上
Object.defineProperty(MyClass2.prototype, 'sayName', updatedDescriptor)

// 测试:
const obj = new MyClass2();
obj.sayName = () => console.log('Test UpdateSayName');  // TypeError: Cannot assign to read only property 'sayName' of object '#<MyClass2>'
// 验证正确(因为在decorateMethodDecorator将其描述改为“不可编辑”)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值