【Nest.js】深入理解装饰器(三)~

接前两篇:

6、类方法装饰器

方法指的是我们加到类上的函数,或者是从超类继承的函数。方法装饰器是这样的

ts 复制代码
class A {
    @MethodDecorator()
    fly(meters: number) {
        // code
    }
}

方法装饰器适用于类的方法,而不适用于函数的参数。

6.1 类方法装饰器

方法装饰器在方法实例化之前被调用。传递给这些函数的参数有

  1. 对于静态成员,可以是类的构造函数;对于实例成员,可以是类的原型。
  2. 给出属性名称的字符串
  3. 成员的 PropertyDescriptor - 函数

前两个装饰器函数参数与其他几种装饰器类型相同。PropertyDescriptor 与其他一些装饰器类型中使用的对象相同,但使用方式略有不同。JavaScript 对该对象的填写方式与对访问器的填写方式不同。

6.2 类方法装饰器函数

TS 4.xTS 5.x 中方法装饰器函数的参数是不一样的:

  1. TS 4.x 中方法 Decorator 如下所示:
ts 复制代码
function methodDecorator(
    target: any, 
    propertyKey: string, 
    descriptor: PropertyDescriptor
    ) {
  // ...
};

stage1 版本的类方法装饰器中,我们可以获取到三个参数:

  1. target:被修饰的类
  2. name:类成员的名字
  3. descriptor:属性描述符,对象会将这个参数传给 Object.definePropert

想要装饰一个函数,我们必须修改 descriptor.value ,然后再把 descriptor 返回回去,下面例子。

  1. TS 5.x 如下所示:
ts 复制代码
type ClassMethodDecorator = (
  value: Function,
  context: {
    kind: 'method';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

发生了很多变化,TS 4.xTS 5.x中的装饰器函数没有 API 兼容性。就我个人而言,我认为TS 5.x有一个更直观的 interface

可以发现相比类装饰器,context 中主要多了三个参数:

  • static:是否威静态方法
  • private:是否为私有方法
  • access:可以获取到方法的 getter 方法(通过它我们就可以公开访问私有方法的字段)

除了方法之外,装饰器也有相应的上下文类型,你也可以使用它们。

请注意,Decorator 函数的返回值可以为 void(即不返回任何内容)或返回新的装饰对象。

返回一个新的装饰对象代替实际的调用。

6.2.1 TypeScript 5 之前的用法

第一步,让我们创建一个装饰器来打印参数值:

ts 复制代码
import * as util from 'util';

function logMethod(
    target: Object, 
    propertyKey: string,
    descriptor: PropertyDescriptor
    ) {

    console.log(`logMethod`, {
        target, propertyKey, descriptor, 
        targetKeys: Object.getOwnPropertyNames(target),
        function: descriptor.value,
        funcText: descriptor.value.toString()
    });
}

class MethodExample {

    @logMethod
    method(x: number) {
        return x * 2;
    }
}

这将打印出目标、描述符和描述符中值字段的可用数据。对于 targetKeys,我们感兴趣的是验证 target 是否是包含方法的类。对于 function,我们了解到值字段包含函数,而使用 toString 可以让我们看到函数的文本。

输出结果:

ts 复制代码
logMethod {
  target: {},
  propertyKey: 'method',
  descriptor: {
    value: [Function: method],
    writable: true,
    enumerable: false,
    configurable: true
  },
  targetKeys: [ 'constructor', 'method' ],
  function: [Function: method],
  funcText: 'method(x) {\n        return x * 2;\n    }'
}

是的,目标显然是包含此方法的类,而值域则是实际函数。

6.2.2 TypeScript 5 之后的用法

有这样一个类:

ts 复制代码
class OnePieceCharacter {
  constructor(private name: string) {}

  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

new OnePieceCharacter('Luffy').greet();
// Luffy is saying hello

通常,如果我们要对 greet 这个方法进行一些调试,你可能会这样写:

ts 复制代码
greet(){
  console.log('LOG: Entering the method');
  console.log(`${this.name} is saying hello`);
  console.log('LOG: Leaving the method');
}

乍看起来,也没那么复杂,但是如果多个方法都需要调试,我们是不是就会写大量的这种重复代码?这个时候就是方法装饰器的用武之地了!

ts 复制代码
function logMethod(originalMethod: any, context: any) {

  // 定义一个新方法
  const replaceMethod = (this: any, ...args: any[]) => {
    console.log('Entering the method'); // 扩展方法
    const result = originalMethod.call(this, ...args); // 调用原来的方法
    console.log('Leaving the method'); // 扩展方法
    return result;
  }

  // 返回装饰后的方法
  return replaceMethod;
}

replaceMethod 中,我们放入日志语句,然后调用 originalMethod。重要的是,我们要使用 .call,并向其传递正确的上下文和参数。

然后,我们就可以在代码中应用装饰器了:

ts 复制代码
class OnePieceCharacter {
  constructor(private name: string) {}

  @logMethod
  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

new OnePieceCharacter('Luffy').greet();
// LOG: Entering the method
// Luffy is saying hello
// LOG: Leaving the method

这样,我们就实现了一个可重用的带有代码调试功能的方法装饰器了!是不是很简单!

再进一步,假如我们希望在每个方法的日志都带有一些前缀,以方便我们识别当前打印的是哪个方法,这就需要给方法装饰器传一个唯一的 prefix 了,装饰器工厂就能干这个,请看这个:

ts 复制代码
type Method = (this: unknown, ...arg: unknown[]) => unknown;

function logWithPrefix(prefix: string) {
  return function actulDecorator(
      method: Method, 
      context: ClassMemberDecoratorContext
    ): Method {
    // target 就是当前被装饰的 class 方法
    const originalMethod = target;

    // 定义一个新方法
    const replaceMethod = (this: any, ...args: any[]) => {
      console.log(`${prefix}: method start`); // 扩展方法
      const result = originalMethod.call(this, args); // 调用原来的方法
      console.log(`${prefix}: method end`); // 扩展方法
      return result;
    }
    
    // 返回装饰后的方法
    return replaceMethod;
  };
}

在之前实现的装饰器外层再包裹一层函数,然后传入 prefix,你可以这样使用:

ts 复制代码
class OnePieceCharacter {
  constructor(private name: string) {}

  @logMethod('DEBUGGER')
  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

new OnePieceCharacter('Luffy').greet();
// DEBUGGER: Entering the method
// Luffy is saying hello
// DEBUGGER: Leaving the method

1)ClassMethodDecoratorContext

使用装饰器做很多事情,你可以在装饰器中加入自定义逻辑,以调整 thisargs。你甚至可以访问装饰函数的上下文。

之前,我们只是用 any 作为装饰函数的参数类型。我们这样做是为了简单起见,但有一些类型可以告诉我们可以访问哪种上下文。

让我们重构一下函数签名:

ts 复制代码
function logMethod(originalMethod: any, context: ClassMethodDecoratorContext) {}

那么问题来了,什么是ClassMethodDecoratorContext?官方给出的类型签名如下:

ts 复制代码
interface ClassMethodDecoratorContext<
  This = unknown,
  Value extends (this: This, ...args: any) => any = (
    this: This,
    ...args: any
  ) => any,
> {
  /** The kind of class member that was decorated. */
  readonly kind: 'method';
  /** The name of the decorated class member. */
  readonly name: string | symbol;
  /** A value indicating whether the class member is a static (`true`) or instance (`false`) member. */
  readonly static: boolean;
  /** A value indicating whether the class member has a private name. */
  readonly private: boolean;
  addInitializer(initializer: (this: This) => void): void;
}

2)addInitializer

除了最后一个addInitializer官方没有给出详细的注释,其他的都有详细的注释说明,那这个addInitializer又是什么?

addInitializer 函数允许我们提供一个回调函数(类似 hooks),用来做初始化。那什么时候有用呢?一旦你开始传递函数,它就会变得有用。来看下面的例子:

ts 复制代码
class OnePieceCharacter {
  constructor(private name: string) {}

  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

const luffy = new OnePieceCharacter('Luffy');
luffy.greet();
// Luffy is saying hello

const myFunc = luffy.greet;
myFunc();
// undefined is saying hello

在第二种方法中,"this" 指向的是全局,而不是 OnePieceCharacter,导致打印出了undefined

写一个小装饰器来解决这个问题:

ts 复制代码
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = context.name;
  if (context.private) {
    throw new Error('bound can not be used on private methods');
  }
  context.addInitializer(function () {
    this[methodName] = this[methodName].bind(this);
  });
}

addInitializer 是通过调用 context.addInitializer(function (){ ... }) 添加到类中的,其中 addInitializer 调用中的匿名函数就是将方法绑定到实例的初始化器。

如果我们现在重新运行示例,会得到以下输出结果:

ts 复制代码
class OnePieceCharacter {
  constructor(private name: string) {}

  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

const luffy = new OnePieceCharacter('Luffy');
luffy.greet();
// Luffy is saying hello

const myFunc = luffy.greet;
myFunc();
// Luffy is saying hello
  • addInitializer 方法会在每次有新的实例被创建,字段被初始化之前被调用,我们可以在这个实际为它绑定 this ,然后就可以将方法单独进行调用了~
  • 需要注意的是:我们虽然用 any 来作为第一个参数的类型,其实是没关系的,因为除了调用原始方法的第一个参数外,我们并没有对它做什么额外的操作。当然,如果你是一个处女座,你也可以创建一个完全类型化的装饰器。

6.3 类方法装饰器的应用

下面使用 TypeScript 5 之前的方法装饰器实现几个常用的装饰器场景。

6.3.1 监听方法调用

因为我们已经为该方法创建了 PropertyDescriptor,所以我们可以尝试覆盖该函数。就像使用访问器一样,让我们试试新的装饰器,它可以让我们监视函数的输入和输出值。

ts 复制代码
function MethodSpy(
    target: Object,
    propertyKey: string, 
    descriptor: PropertyDescriptor
) {

    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`MethodSpy before ${propertyKey}`, args);
        const result = originalMethod.apply(this, args);
        console.log(`MethodSpy after ${propertyKey}`, result);
        return result;
    }
}

class SpiedOn {

    @MethodSpy
    area(width: number, height: number) {
        return width * height;
    }

    @MethodSpy
    areaCircle(diameter: number) {
        return Math.PI * ((diameter / 2) ** 2);
    }
}

const spyon = new SpiedOn();

console.log(spyon.area(6, 10));
console.log(spyon.area(16, 20));

console.log(spyon.areaCircle(10));
console.log(spyon.areaCircle(20));

在函数内部,我们保存了 descriptor.value 的原始值,因为这是实际的成员函数。我们将其替换为另一个可接受任意数量参数的函数。请记住,...args 将成为一个数组,其中包含传递给函数的参数。我们首先打印函数名称和提供的参数。然后,我们调用原始方法,提供参数并捕获结果。然后打印函数名和结果,最后返回结果。

然后运行脚本:

ts 复制代码
MethodSpy before area [ 6, 10 ]
MethodSpy after area 60
60
MethodSpy before area [ 16, 20 ]
MethodSpy after area 320
320
MethodSpy before areaCircle [ 10 ]
MethodSpy after areaCircle 78.53981633974483
78.53981633974483
MethodSpy before areaCircle [ 20 ]
MethodSpy after areaCircle 314.1592653589793
314.1592653589793

在每种情况下,前阶段打印的值是一个数组,后阶段打印的值是每个方法的预期结果。

由于其编写方式,我们可以轻松地将其应用于任何方法的任何装饰器。它甚至能自动获取方法名称。

6.3.2 身份验证

假设我们正在构建一个网络应用程序,该程序要求对某些路由进行身份验证。我们可以创建一个 @auth 装饰器,在允许访问路由之前检查用户是否通过身份验证:

ts 复制代码
function auth(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const isAuthenticated = checkIfUserIsAuthenticated();
    if (!isAuthenticated) {
      // 重定向到登录页或者抛出错误提示
      return;
    }
    originalMethod.apply(this, args);
  };
  return descriptor;
}

现在,我们可以将 @auth 装饰器应用于任何需要身份验证的方法:

ts 复制代码
class MyRoutes {
  @auth
  getProfile() {
    // ...
  }

  @auth
  updateProfile(profileData: any) {
    // ...
  }
}

通过这种设置,@auth 装饰器将在允许访问 getProfile()updateProfile() 方法之前自动检查用户是否已通过身份验证。

6.4 方法和参数装饰器协同工作

我们已经提出,这两种装饰器可以共同产生有用的结果。我们要尝试的概念是参数装饰器,它可以在未提供可选参数的情况下提供默认值。也就是:

ts 复制代码
class DefaultExample {
    @SetDefaults
    volume(
        z: number,
        @ParamDefault<number>(10) x?: number,
        @ParamDefault<number>(15) y?: number,
        title?: string
    ) {
        const ret = {
            x, y, z, volume: x * y * z, title
        };
        console.log(`volume `, ret);
        return ret;
    }
}

类,它有一个根据 x、y 和 z 值计算体积的方法。@ParamDefault 装饰器可以让我们指定一个默认值。请注意,参数中的? 表示它是可选的。因此,我们需要一些代码来检测是否存在未提供的参数,并进行替换。

由于 @ParamDefault 是一个参数装饰器,因此除了将其存在记录到数组中外,它不能做任何其他事情,正如我们在学习参数装饰器时所讨论的那样。要替换默认值,我们需要一些针对类实例执行的代码。我们刚刚展示了方法装饰器可以覆盖方法的函数,以便针对类实例执行。我们的计划是注入一个函数,在实际函数之前调用,并使用注入的函数设置默认值。

让我们从 ParamDefault 开始,看看如何实现这一切:

ts 复制代码
const paramDefaults = [];
...
function ParamDefault<T>(value: T) {
    return (target: Object, propertyKey: string | symbol,
        parameterIndex: number) => {

        paramDefaults.push({
            target, propertyKey, parameterIndex, value
        });
    }
}

This stores data about the parameter that is decorator, as well as the default value to use. The `paramDefaults` array stores this data.

function findDefaults(target: Object, propertyKey: string) {
    const ret = [];
    for (const def of paramDefaults) {
        if (target === def.target && propertyKey === def.propertyKey) {
            ret.push(def);
        }
    }
    return ret;
}

function SetDefaults(target: Object, propertyKey: string,
    descriptor: PropertyDescriptor) {

    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`SetDefaults before ${propertyKey}`, args);

        for (const def of findDefaults(target, propertyKey)) {
            if (typeof args[def.parameterIndex] === 'undefined'
             || args[def.parameterIndex] === null) {
                args[def.parameterIndex] = def.value;
            }
        }

        console.log(`SetDefaults after substitution ${propertyKey}`, args);
        const result = originalMethod.apply(this, args);
        console.log(`SetDefaults after ${propertyKey}`, result);
        return result;
    }
}

SetDefaults 装饰器函数安装了一个函数,该函数将根据已声明的默认值处理替换值。findDefaults 函数会在 paramDefaults 中搜索与目标和属性键匹配的所有项目。选定默认值后,我们可以查看实际参数中是否有未提供的参数,我们将其定义为未定义或空值。如果没有提供参数,则进行替换,然后调用原始函数并返回结果。

回到我们的例子,有四种组合可以尝试:

  1. 不提供 x 和 y 值。
  2. 仅 x 值未提供。
  3. 只不提供 y 值。
  4. 同时提供 x 和 y 值。

这样就处理了为缺失参数替换值的所有四种可能性。此外,我们还添加了另一个参数 title,在这里我们不提供默认值,以验证是否存在缺失。最后,参数 z 作为一个非选项参数,以确保正确处理此类值。

这就是我们的测试代码:

ts 复制代码
const de = new DefaultExample();

// both x and y missing
console.log(de.volume(10));
console.log('----------------------');
// only x missing
console.log(de.volume(20, null, 20, "Second"));
console.log('----------------------');
// only y missing
console.log(de.volume(30, 30, null));
console.log('----------------------');
// both x and y supplied
console.log(de.volume(40, 40, 50, "Fourth"));

它可以处理刚才提到的四种情况,并确保为每个参数传递一个不同的值,以确保在调用之间不会出现数值失衡。

结果如下:

ts 复制代码
SetDefaults before volume [ 10 ]
SetDefaults after substitution volume [ 10, 10, 15 ]
volume  { x: 10, y: 15, z: 10, volume: 1500, title: undefined }
SetDefaults after volume { x: 10, y: 15, z: 10, volume: 1500, title: undefined }
{ x: 10, y: 15, z: 10, volume: 1500, title: undefined }
----------------------
SetDefaults before volume [ 20, null, 20, 'Second' ]
SetDefaults after substitution volume [ 20, 10, 20, 'Second' ]
volume  { x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
SetDefaults after volume { x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
{ x: 10, y: 20, z: 20, volume: 4000, title: 'Second' }
----------------------
SetDefaults before volume [ 30, 30, null ]
SetDefaults after substitution volume [ 30, 30, 15 ]
volume  { x: 30, y: 15, z: 30, volume: 13500, title: undefined }
SetDefaults after volume { x: 30, y: 15, z: 30, volume: 13500, title: undefined }
{ x: 30, y: 15, z: 30, volume: 13500, title: undefined }
----------------------
SetDefaults before volume [ 40, 40, 50, 'Fourth' ]
SetDefaults after substitution volume [ 40, 40, 50, 'Fourth' ]
volume  { x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
SetDefaults after volume { x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }
{ x: 40, y: 50, z: 40, volume: 80000, title: 'Fourth' }

虚线是为了读取结果时更加清晰。仔细阅读这些结果,你会发现所有的替换都如预期发生。

6.5 小结

我们通过方法和参数装饰器的组合实现了一个非常有趣的功能,即为缺失的方法参数替换默认值。参数装饰器函数保存了有关默认值的数据,而方法装饰器函数则检测到有默认值的缺失参数。

7、混合装饰器(Hybrid Decorators)

在本文中,我们将讨论 TypeScript 装饰器函数中使用的各种函数签名。通过仔细检查参数,我们可以推断出装饰器所连接的对象类型。这是因为,如果我们查看装饰器函数签名的完整列表,就会发现其中存在一种模式。这样,我们就可以定义类型保护函数来检测使用的是哪种模式,从而可靠地知道装饰器所连接的对象类型。

有了用于测试调用装饰器函数的函数签名的函数,我们就能确定装饰器所连接的对象类型。这样,装饰器就可以用于任何对象类型。本文的目标是提供一种可在所有五种上下文中使用的装饰器:类、属性、访问器、参数和方法。

7.1 TypeScript 装饰器函数方法签名概述

回顾一下本系列其他文章中使用的装饰器函数签名,我们就能得到这些签名:

ts 复制代码
const accessorfunc = (target: Object,
                        propertyKey: string,
                        descriptor: PropertyDescriptor) => {};

const constructorfunc = (constructor: Function) => {};

const methodsfunc = (target: Object, propertyKey: string,
                    descriptor: PropertyDescriptor) => {};

const parametersfunc = (target: Object,
                        propertyKey: string | symbol,
                        parameterIndex: number) => {};

const propertiesfunc = (target: Object, member: string)  => {};

例如,class 装饰器只需要一个参数,可以命名为 target。属性装饰器只需要两个参数。其他三个装饰器需要三个参数,它们之间存在差异。参数装饰器的第三个参数是一个数字。访问器和方法装饰器的第三个参数都是 PropertyDescriptor,但构造方法有所不同。

连接到访问器的装饰器接收一组参数,而连接到类的装饰器接收其他参数。每种装饰器类型的装饰器函数都必须能够处理任何一组参数,并能区分一种用途或另一种用途。

这意味着我们需要一个与每种装饰器类型相匹配的通用函数签名。

经过考虑和测试,我们决定使用这种方法来处理每种情况:

ts 复制代码
(target: Object, propertyKey?: string | symbol, descriptor?: number | PropertyDescriptor)

目标参数对每个参数都是通用的,而其他两个参数则是可选的。另外两个参数的值也有变化。本签名处理所有变量。

这将是一个装饰器函数的函数签名,可用于五种装饰器类型中的任何一种。接下来需要类似类型保护的函数来测试参数。

请记住,在 TypeScript 中,类型保护函数接收一个参数,并测试该参数以确保其属于特定类型。

7.2 混合装饰函数概念的测试用例

创建该类定义是为了测试混合装饰函数是否可行。

ts 复制代码
@Decorator
class HybridDecorated {
    @Decorator
    prop1: number;

    @Decorator
    prop2: string;

    @Decorator
    method(
        @Decorator param1: string,
        @Decorator param2: string
    ) {
        console.log(`inside method function`);
        return { param1, param2 };
    }

    #meaning: number = 42;

    @Decorator
    get meaning() { return this.#meaning; }
    set meaning(nm: number) { this.#meaning = nm; }
}

这就需要一个名为 "装饰器" 的函数,它能成功检测出所处的上下文环境,或者换一种说法,即装饰器函数所连接的对象类型。

7.3 原型化装饰器以处理所有 TypeScript 装饰器用途

鉴于前面展示的通用函数签名,必须这样定义装饰器函数:

ts 复制代码
function Decorator(target: Object, 
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) {

    console.log(`Decorator target`, target);
    console.log(`Decorator propertyKey`, propertyKey);
    console.log(`Decorator descriptor`, descriptor);
}

事实上,如果构建一个包含上述类定义和该装饰器实现的文件(例如 first.ts),就会得到这样的输出结果:

ts 复制代码
Decorator target {}
Decorator propertyKey prop1
Decorator descriptor undefined
Decorator target {}
Decorator propertyKey prop2
Decorator descriptor undefined
Decorator target {}
Decorator propertyKey method
Decorator descriptor 1
Decorator target {}
Decorator propertyKey method
Decorator descriptor 0
Decorator target {}
Decorator propertyKey method
Decorator descriptor {
  value: [Function: method],
  writable: true,
  enumerable: false,
  configurable: true
}
Decorator target {}
Decorator propertyKey meaning
Decorator descriptor {
  get: [Function: get meaning],
  set: [Function: set meaning],
  enumerable: false,
  configurable: true
}
Decorator target [class HybridDecorated]
Decorator propertyKey undefined
Decorator descriptor undefined

这证明了这个概念是可行的,TypeScript 装饰器函数可以成功地附加到所有五种可装饰对象类型上。

7.4 测试混合装饰器所连接的对象类型

为了让这些函数更简单一些,我们从下面这个函数开始:

ts 复制代码
const isset = (val) => {
    return typeof val !== 'undefined' && val !== null;
};
const notset = (val) => {
    return (typeof val === 'undefined') || (val === null);
};

用于确保参数确实有一个值。如果参数不是未定义的,也不是空值,那么它就有一个值。

ts 复制代码
export const isClassDecorator = (target: Object,
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) => {

    return (isset(target)
         && notset(propertyKey)
         && notset(descriptor));
};

类装饰器的第一个参数只有一个值。

ts 复制代码
export const isPropertyDecorator = (target: Object,
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) => {

    return (isset(target)
         && isset(propertyKey)
         && notset(descriptor));
};

属性装饰器只有前两个参数值。

ts 复制代码
export const isParameterDecorator = (target: Object,
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) => {

    return (isset(target)
         && isset(propertyKey)
         && isset(descriptor)
         && typeof descriptor === 'number');
};

参数装饰器的三个参数都有值,第三个是数字。

ts 复制代码
export const isMethodDecorator = (target: Object,
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) => {

    if ((isset(target)
     && isset(propertyKey)
     && isset(descriptor)
     && typeof descriptor === 'object')) {
        const propdesc = <PropertyDescriptor>descriptor;
        return (typeof propdesc.value === 'function');
    } else {
        return false;
    }
}

方法装饰器的三个参数都有值,第三个参数是 PropertyDescriptor 对象。该描述符的值域中存储了一个函数。

ts 复制代码
export const isAccessorDecorator = (target: Object,
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) => {

    if ((isset(target)
     && isset(propertyKey)
     && isset(descriptor)
     && typeof descriptor === 'object')) {
        const propdesc = <PropertyDescriptor>descriptor;
        return (typeof propdesc.value !== 'function')
         && (typeof propdesc.get === 'function'
          || typeof propdesc.set === 'function');
    } else {
        return false;
    }
}

访问器装饰器与方法装饰器类似。区别在于 value 字段没有函数,而 get 和/或 set 字段有函数。

7.5 在混合装饰器函数中使用装饰器类型保护

下面我们来演示如何使用这些函数构建一个装饰器函数,以处理所有五种情况:

ts 复制代码
import {
    isClassDecorator, isPropertyDecorator, isParameterDecorator,
    isMethodDecorator, isAccessorDecorator
} from 'decorator-inspectors';

function Decorator(target: Object, 
    propertyKey?: string | symbol,
    descriptor?: number | PropertyDescriptor) {

    if (isClassDecorator(target, propertyKey, descriptor)) {
        console.log(`Decorator called on class`, target);
    } else if (isPropertyDecorator(target, propertyKey, descriptor)) {
        console.log(`Decorator called on property ${target} ${String(propertyKey)}`);
    } else if (isParameterDecorator(target, propertyKey, descriptor)) {
        console.log(`Decorator called on parameter ${target} ${String(propertyKey)} ${descriptor}`);
    } else if (isMethodDecorator(target, propertyKey, descriptor)) {
        console.log(`Decorator called on method ${target} ${String(propertyKey)}`, descriptor);
    } else if (isAccessorDecorator(target, propertyKey, descriptor)) {
        console.log(`Decorator called on accessor ${target} ${String(propertyKey)}`, descriptor);
    }
    else {
        console.error(`Decorator called on unknown thing`, target);
        console.error(`Decorator called on unknown thing`, propertyKey);
        console.error(`Decorator called on unknown thing`, descriptor);
    }
}

函数 Decorator 使用通用的 decorator 方法签名。然后,它会使用这些函数来确定在哪种上下文中使用,并打印适当的信息。如果使用不当,则会打印底部的信息。

如果我们在上图所示的示例类中使用该装饰器函数,就会得到以下输出结果:

ts 复制代码
Decorator called on property [object Object] prop1
Decorator called on property [object Object] prop2
Decorator called on parameter [object Object] method 1
Decorator called on parameter [object Object] method 0
Decorator called on method [object Object] method {
  value: [Function: method],
  writable: true,
  enumerable: false,
  configurable: true
}
Decorator called on accessor [object Object] meaning {
  get: [Function: get meaning],
  set: [Function: set meaning],
  enumerable: false,
  configurable: true
}
Decorator called on class [class HybridDecorated]

正如您所看到的,所有内容都已正确识别。打印消息的顺序恰好与深入介绍使用和实现 TypeScript 装饰器时讨论的评估顺序一致。

7.6 小结

在查看不同的 TypeScript 装饰器软件包时,我们发现同一个装饰器名称经常被用于多个对象类型。为此,装饰器必须检查其参数,以确定在哪种上下文中使用。这些函数将帮助您在装饰器函数中实现同样的功能。

8、为什么要使用装饰器

通过上面,想必对装饰器有了一个比较深入的认知了,那为什么要使用装饰器呢?总结下来,主要有以下几点:

  1. 可重用性:装饰器允许你封装特定的行为或功能,并将其应用到多个类、方法、访问器、属性或参数声明中,从而轻松地在代码库中重用相同的功能。
  2. 抽象:装饰器允许你抽象出复杂的逻辑,并以一种更具声明性的方式将其应用到代码中,从而使代码更易于理解和维护。
  3. 模块化:装饰器可以将代码分解成更小、更易于管理的部分,从而使测试和调试变得更容易。
  4. 依赖注入:装饰器广泛应用于 NestJS、Angular 和 Spring 等依赖注入框架。这些框架使用装饰器将依赖注入类的过程自动化。
  5. 面向切面编程(AOP):装饰器可让您以简洁、有序的方式在代码中添加日志、缓存和安全等跨领域问题。
  6. 加快开发速度:通过使用装饰器,您可以轻松地实现以下功能,从而加快开发过程

参考资料


相关推荐
咔叽布吉1 小时前
【前端学习笔记】ES6 新特性
前端·笔记·学习
推开世界的门2 小时前
web 中 canvas 污染 以及解决方案
前端
星离~2 小时前
css—轮播图实现
前端·css
龙雨LongYu122 小时前
vue3+ts 我写了一个跟swagger.yml生成请求和响应实体(接口)
前端·vue.js·typescript
Stanford_11063 小时前
关于IDE的相关知识之一【使用技巧】
前端·ide·windows·微信小程序·微信公众平台·twitter·微信开放平台
_志哥_3 小时前
web开发环境下启动HTTPS服务访问
前端·javascript·https
爱健身的小刘同学3 小时前
安装 electron 依赖报错
前端·javascript·electron
耶啵奶膘3 小时前
uniapp+vue2+uview2.0导航栏组件二次封装
前端·javascript·uni-app
布兰妮甜3 小时前
如何使用 Tailwind CSS 构建响应式网站:详细指南
前端·css·tailwind css
MavenTalk3 小时前
前端技术选型之uniapp
android·前端·flutter·ios·uni-app·前端开发