现在编写更干净、更智能的代码

探索 TypeScript 装饰器:提升代码质量的新途径

TypeScript 是一种建立在 JavaScript 之上的强类型编程语言,旨在提供更强大的工具,尤其是在处理大型项目时。 它通过引入类型系统来弥补 JavaScript 的不足,从而帮助开发者解决在 JavaScript 编码中遇到的一些常见问题。

在 TypeScript 中,每个值都有其明确的类型。 TypeScript 会进行类型检查,确保每个值都符合其类型规则。 这种检查在代码执行前进行,这意味着 TypeScript 可以在开发阶段就发现潜在的错误,无需实际运行程序。

这种机制称为静态类型检查,它通过检查程序中使用的值的类型,来帮助开发者在早期发现并修复错误。

除了静态类型检查外,TypeScript 还提供了许多其他功能,以提高代码的可读性、可重用性和可维护性。 其中,装饰器是 TypeScript 中一个强大的特性。

TypeScript 装饰器详解

TypeScript 中的装饰器是一种特殊的声明,它允许在运行时修改或增强代码的行为,或者向代码添加元数据。 装饰器为 TypeScript 带来了元编程的能力,使程序能够将其他程序视为数据,并以此修改其行为。

本质上,装饰器是函数,当访问或修改被装饰的元素时,这些函数会被调用以执行特定的逻辑。 通过这种方式,可以将额外的功能添加到被装饰的元素中。 TypeScript 装饰器可以应用于类声明、方法、属性、访问器(getter 和 setter)以及方法参数。

在 TypeScript 中,装饰器以 @ 符号开头,其形式为 @expression,其中 expression 的结果是一个函数,该函数将在运行时被调用。 使用装饰器的基本语法如下:

@装饰器名称
要装饰的元素

以下是一个简单的类装饰器示例:

function logClass(target: Function) {
  console.log("类装饰器已被调用");
  console.log("类:", target);
}

@logClass // @logClass 是一个装饰器
class MyClass {
  constructor() {
    console.log("MyClass 的实例已被创建");
  }
}

const myInstance = new MyClass();

执行以上代码的结果如下:

输出:

类装饰器已被调用
类: [class MyClass]
MyClass 的实例已被创建

logClass() 函数接收一个类型为 Function 的参数 targettarget 参数的类型是 Function,因为它接收的是被装饰类的构造函数。

要将 logClass() 函数用作装饰器来装饰名为 MyClass 的类,我们需要在 MyClass 声明之前添加 @logClass。 装饰器的名称必须与用于装饰元素的函数名称相同。

当创建 MyClass 的实例时,装饰器的逻辑会被执行,如输出所示,它会在类构造函数执行之前被调用。

目前,装饰器在 TypeScript 中仍然是一个实验性功能。 因此,要在 TypeScript 中使用装饰器,需要在 tsconfig.json 文件的编译器选项中启用 experimentalDecorators 选项。

为此,请在您的项目目录中打开终端,执行以下命令以在 TypeScript 项目文件夹中生成 tsconfig.json 文件:

tsc --init

获得 tsconfig.json 文件后,将其打开并取消注释 experimentalDecorators,如下所示:

此外,请确保 JavaScript 的目标版本至少设置为 ES2015。

TypeScript 装饰器的重要性

优秀的代码可以通过其可读性、可重用性和可维护性来衡量。 可读的代码易于理解和解释,能够清晰地表达开发者的意图。

另一方面,可重用代码允许特定组件(例如函数和类)在应用程序的不同部分或全新的应用程序中重用、调整和使用,而无需进行重大修改。

可维护的代码是指在其生命周期内可以轻松修改、更新和修复的代码。

TypeScript 装饰器使您能够实现代码的可读性、可重用性和可维护性。 首先,装饰器允许您使用更具声明性的语法来增强代码行为,使代码更易于阅读。 您可以将逻辑封装在装饰器中,并通过在需要该逻辑的地方装饰代码的不同元素来调用它们。

这使得您的代码更易于理解,并清楚地传达了开发者的意图。

装饰器不是一次性的元素; 它们本质上是可重用的。 您可以创建一次装饰器,并在多个地方多次使用它。 因此,您可以在代码库中定义、导入装饰器,并在任何想要修改代码行为的地方使用它们。 这样做可以避免代码库中的重复逻辑,从而增强代码的可重用性。

装饰器还为您的代码提供了极大的灵活性和模块化,允许您将不同的功能分离为独立的组件。 总而言之,它们促进了可读和可重用代码的编写,意味着 TypeScript 装饰器能够帮助您拥有易于维护的代码。

TypeScript 装饰器的类型

正如前面提到的,TypeScript 装饰器可以附加到类、类属性、类方法、类访问器和类方法参数。 因此,我们可以根据装饰的元素来区分不同类型的 TypeScript 装饰器。 这些装饰器包括:

#1. 类装饰器

类装饰器用于观察、修改或替换类定义。 它在它所装饰的类之前声明。 类装饰器应用于它所装饰的类的构造函数。 在运行时,类装饰器会被调用,并且接收被装饰类的构造函数作为其唯一的参数。

以下是一个用于防止类被扩展的类装饰器的示例:

function frozen(target: Function) {
  Object.freeze(target);
  Object.freeze(target.prototype)
}

@frozen
class Vehicle {
  wheels: number = 4;
  constructor() {
    console.log("车辆已创建")
  }
}

class Car extends Vehicle {
  constructor() {
    super();
    console.log("汽车已创建");
  }
}

console.log(Object.isFrozen(Vehicle));

为了防止类被扩展,我们使用 Object.freeze() 函数,并将想要冻结的类作为参数传递给它。 装饰器用于向类添加此功能。 我们可以通过将类传递给 isFrozen() 来检查 Vehicle 类是否在运行时被冻结。代码的输出如下:

true

#2. 属性装饰器

属性装饰器用于装饰类属性,它在属性装饰之前声明。 属性装饰器可用于更改或观察属性定义。

在运行时,装饰器会被调用,它接收两个参数。 第一个参数是类的构造函数,如果成员是静态的,或者是类的原型,如果成员是实例成员。 第二个参数是成员的名称,也就是您要装饰的属性。

在 TypeScript 中,静态成员前面有关键字 static。 无需实例化类即可访问静态成员。 实例成员前面没有关键字 static,并且只能在创建类的实例后才能访问。

以下是一个属性装饰器的示例:

function wheelsDecorator(target: any, propertyName: string) {
  console.log(propertyName.toUpperCase())
}

class Vehicle {
  @wheelsDecorator
  wheels: number = 4;
  constructor() {
    console.log("车辆已创建")
  }
}

运行代码的输出如下:

WHEELS

#3. 方法装饰器

方法装饰器在方法声明之前声明,用于观察、修改或替换方法定义。 它接收三个参数:第一个是类的构造函数(如果成员是静态的),或者是类的原型(如果成员是实例成员)。

第二个参数是成员的名称,最后一个参数是成员的属性描述符。 属性描述符是一个与对象属性关联的对象,它提供有关属性的属性和行为的信息。

当您想要在调用方法之前或之后执行某些操作时,方法装饰器会非常有用。 我们可以使用它们来记录有关被调用方法的信息。 如果您想通知用户某个方法已被弃用,这会很有用; 也就是说,它仍然可以使用,但不建议您使用它,因为它可能会被删除。

以下是一个方法装饰器的示例:

const logDeprecated =(target: any, methodName: string, descriptor: PropertyDescriptor) => {
  console.log(`${methodName} 已被弃用`)
  console.log(descriptor);
}

class Vehicle {
  wheels: number = 4;
  constructor() {
    console.log("车辆已创建")
  }
  @logDeprecated
  reFuel(): void {
    console.log("您的车辆正在加油");
  }
}

输出:

reFuel 已被弃用
{
  value: [Function: reFuel],
  writable: true,
  enumerable: false,
  configurable: true
}

#4. 访问器装饰器

在 TypeScript 中,有两种类型的访问器方法:getset。 访问器方法用于控制对类属性的访问。 访问器装饰器用于装饰这两种访问器方法,它们在访问器声明之前声明。 因为访问器仍然是方法,所以访问器装饰器的工作方式与方法装饰器类似。

以下是一个访问器装饰器的示例:

const logWheels =(target: any, accessorName: string, descriptor: PropertyDescriptor) => {
  console.log(`${accessorName} 用于获取轮子数量`)
  console.log(descriptor);
}

class Vehicle {
  private wheels: number = 4;
  constructor() {
    console.log("车辆已创建")
  }
  @logWheels
  get numWheels(): number {
    return this.wheels;
  }
}

输出:

numWheels 用于获取轮子数量
{
  get: [Function: get numWheels],
  set: undefined,
  enumerable: false,
  configurable: true
}

对于访问器装饰器,需要注意的是,装饰器不能应用于多个同名的 get/set 访问器。 例如,在上面的代码示例中,如果您创建一个名为 set numWheels 的 setter,则不能在其上使用 logWheels 装饰器。

#5. 参数装饰器

参数装饰器用于观察方法上是否已声明参数,并且它是在参数声明之前声明的。 参数装饰器接收三个参数,第一个是静态成员的类的构造函数,或者是实例成员的类的原型。

第二个参数是成员的名称,也就是参数的名称。 第三个参数是函数参数列表中参数的索引。 也就是说,在参数列表中,第一个参数的索引为 0,这个参数在什么位置?

以下是一个参数装饰器的示例:

const passengerLog = (target: Object, propertyKey: string, parameterIndex: number) => {
  console.log(`装饰器在 ${propertyKey} 的参数索引 ${parameterIndex}`);
}

class Vehicle {
  private wheels: number = 4;
  constructor() {
    console.log("车辆已创建")
  }
  pickPassenger( location: string, numPassengers: string, @passengerLog driver: string) {
    console.log(`${numPassengers} 在 ${location} 由 ${driver} 接载`)
  }
  dropPassenger(driver: string, @passengerLog location: string, numPassengers: string) {
    console.log(`${numPassengers} 在 ${location} 由 ${driver} 送达`)
  }
}

输出:

装饰器在 pickPassenger 的参数索引 2
装饰器在 dropPassenger 的参数索引 1

结论

TypeScript 装饰器在增强代码可读性和帮助您编写模块化、可重用代码方面大有帮助,因为您可以声明一次装饰器并多次使用它们。 此外,装饰器有助于提高代码的整体可维护性。

尽管装饰器仍然是一个实验性功能,但它们非常有用,您绝对应该考虑熟悉它们。

您还可以阅读如何在 TypeScript 中将字符串转换为数字。