TypeScript装饰器学习

随着 TypeScript 和 ES6 中类的引入,现在存在某些场景需要额外的功能来支持标注或修改类和类成员。 装饰器提供了一种为类声明和成员添加标注和元编程语法的方法。接下来记录下装饰器的学习。

环境搭建

安装ts-node,这个工具可以直接执行 TypeScript 文件,而不需要将其编译为 JavaScript 文件。

然后运行tsc --init生成tsconfig.jsontsconfig.json是一个配置文件,用于配置 TypeScript 编译器(tsc)如何处理我们的 TypeScript 代码,由于JavaScript里的装饰器还处于建议征集的第二阶段,可以在TypeScript里已做为一项实验性特性予以支持。需要在tsconfig.json里启用experimentalDecorators编译器选项:

json 复制代码
{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}
​
基本用法
typescript 复制代码
@testable
class MyTestableClass {
  // ...
  static age:Number = 18
}
​
function testable(target:any) {
  target.age = 19;
}
​
console.log(MyTestableClass.age) //19

上面就是一个很简单的实例,可以说装饰器就是一个对类进行处理的函数,他的第一个参数就是我们的目标类,还可以通过装饰器工厂来增加参数:

typescript 复制代码
@testable(20)
class MyTestableClass {
  // ...
  static age: Number = 18
}
​
function testable(age: number) {
  return function (target: any) {
    target.age = age;
  }
}
​
console.log(MyTestableClass.age)
​

接下来更加系统的学习下ts装饰器的种类。

类装饰器

类装饰器通常在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

比如上面的testable就是一个类装饰器,接下来写一个重载构造函数的例子:

scala 复制代码
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}
​
@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}
​
console.log(new Greeter("world"));

这个例子是冴羽大佬写的,由于垃圾的ts水平,要借助GPT才能看懂,搬运下:

这是一个 TypeScript 中的类装饰器函数,它接受一个类的构造函数 constructor 作为参数,并返回一个经过装饰的新类。

  1. function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T):这是函数的签名,它定义了一个泛型函数 classDecorator,接受一个构造函数 constructor 作为参数。这个构造函数必须满足一个条件,即必须具有一个可以接受任意参数的构造函数,因为它应该是一个类的构造函数。
  2. return class extends constructor { ... }:这部分是函数的主体,它返回一个新的类,这个类是通过继承传入的 constructor 构造函数创建的。在这个新的类内部,可以添加新的属性和方法,或者重写原始类的属性和方法。
  3. newProperty = "new property";:这行代码在新的类中添加了一个名为 newProperty 的属性,并给它赋了初始值 "new property"。
  4. hello = "override";:这行代码重写了原始类的属性 hello,并将其值设置为 "override"。

总结一下,这个装饰器函数的作用是创建一个新的类,该类继承自传入的构造函数(原始类),并在新类中添加了新的属性 newProperty,以及重写了原始类的属性 hello。当你应用这个装饰器函数时,原始类的实例将拥有新类的属性和方法,同时保留了原始类的功能。这允许你在不修改原始类的情况下扩展其功能或添加新的

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。

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

  1. target:表示被装饰方法所属的类的构造函数。对于实例方法,target 是类的构造函数;对于静态方法,target 是类本身。
  2. propertyKey:表示被装饰的方法的名称。对于实例方法,propertyKey 是方法的名称;对于静态方法,也是方法的名称。
  3. descriptor:是一个包含了被装饰方法属性描述的对象。它通常包括 valuewritableenumerableconfigurable 等属性。descriptor.value 包含了被装饰方法的原始函数。

可以看下接下来的例子:

typescript 复制代码
function replaceMethod(target:any, propertyKey:string, descriptor:any) {
  const originalMethod = descriptor.value;
​
  descriptor.value = function() {
    return `How are you, ${this.name}?`;
  };
​
  return descriptor;
}
​
class Person {
  name:string
  constructor(name:string) {
    this.name = name;
  }
​
  @replaceMethod
  hello() {
    return `Hi ${this.name}!`;
  }
}
​
const robin = new Person('Robin');
​
console.log(robin.hello()); // 输出:How are you, Robin?

可以理解为方法装饰器返回了函数时,就是替换了原来的方法。

访问器装饰器

访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义。

访问器属性接受三个参数:

  1. target:表示被装饰的类的原型(对于实例成员)或构造函数本身(对于静态成员)。
  2. propertyKey:表示被装饰的成员的名称,通常是一个字符串,对于实例成员就是类的原型上的属性名称,对于静态成员就是类本身的属性名称。
  3. descriptor:是一个包含了被装饰成员属性描述的对象。这个 descriptor 对象通常包括 getset 方法,允许你访问和修改访问器的行为。

看这个例子:

typescript 复制代码
function MyAccessorDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 在这里可以访问和修改访问器的行为
    const originalGet = descriptor.get;
    descriptor.get = function () {
        console.log(`Getting value of ${propertyKey}`);
        return originalGet!.call(this);
    };
}
​
class MyClass {
    private _myProperty: string = "initial value";
​
    @MyAccessorDecorator
    get myProperty(): string {
        return this._myProperty;
    }
​
    set myProperty(value: string) {
        this._myProperty = value;
    }
}
​
const instance = new MyClass();
console.log(instance.myProperty); // 输出并调用装饰器中的修改后的get方法
//Getting value of myProperty
//initial value
成员变量装饰器

也可以称之为属性装饰器,接受两个参数:

  1. target:表示被装饰的类的原型(对于实例属性)或构造函数本身(对于静态属性)。
  2. propertyKey:表示被装饰的属性的名称,通常是一个字符串,对于实例属性就是类的原型上的属性名称,对于静态属性就是类本身的属性名称。

实例:

typescript 复制代码
function MyPropertyDecorator(target: any, propertyKey: string) {
    // 在这里可以访问和修改属性的行为
    console.log(`Decorating property ${propertyKey}`);
}
​
class MyClass {
    @MyPropertyDecorator
    myProperty: string = "initial value";
}
​
const instance = new MyClass();
console.log(instance.myProperty); // 输出并调用装饰器中的逻辑
参数装饰器

参数装饰器接受三个参数:

  1. target:表示被装饰的方法的类的原型(对于实例方法)或构造函数本身(对于静态方法)。
  2. methodName:表示被装饰的方法的名称。
  3. parameterIndex:表示被装饰的参数在方法参数列表中的索引。

也是比较简单的,看下例子:

less 复制代码
function MyParameterDecorator(target: any, methodName: string, parameterIndex: number) {
    // 在这里可以访问和修改参数的行为
    console.log(`Decorating parameter ${parameterIndex} of method ${methodName}`);
}
​
class MyClass {
    myMethod(@MyParameterDecorator param1: string, @MyParameterDecorator param2: number) {
        // 方法体
    }
}
​
const instance = new MyClass();
instance.myMethod("hello", 42);
元数据

我们可以通过reflect-metadata在运行时添加和读取元数据,reflect-metadata 是 TypeScript 中的一个实验性特性,它提供了一种在运行时添加和读取元数据(metadata)的能力。这个特性允许你在类、方法、属性以及参数等各种程序实体上,动态地添加元数据,并在运行时访问这些数据。

在 TypeScript 中,reflect-metadata 主要包括以下几个关键的 API:

  1. Reflect.defineMetadata(key, value, target, propertyKey):用于在指定的目标上定义元数据。key 是元数据的键,value 是元数据的值,target 表示要添加元数据的目标,propertyKey 表示目标上的属性或方法的名称。
  2. Reflect.getMetadata(key, target, propertyKey):用于从指定的目标上获取元数据。key 是元数据的键,target 表示要获取元数据的目标,propertyKey 表示目标上的属性或方法的名称。
  3. Reflect.hasMetadata(key, target, propertyKey):用于检查指定的目标是否包含特定键的元数据。key 是元数据的键,target 表示要检查的目标,propertyKey 表示目标上的属性或方法的名称。
  4. Reflect.metadata(key, value):这是一个装饰器工厂函数,用于将元数据添加到类的属性或方法上。你可以在类成员上使用 @Reflect.metadata(key, value) 装饰器来定义元数据。

在 TypeScript 中,要使用 reflect-metadata 特性,你需要确保在 tsconfig.json 中开启 experimentalDecoratorsemitDecoratorMetadata 这两个编译器选项:

json 复制代码
{
    "compilerOptions": {
        "target": "es5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

同时npm install reflect-metadata安装reflect-metadata包,就可以学习了,看下这个例子:

javascript 复制代码
import "reflect-metadata";
​
class MyClass {
    @Reflect.metadata("custom:annotation", "someValue")
    myMethod() {
        // 方法体
    }
}
​
const metadata = Reflect.getMetadata("custom:annotation", MyClass.prototype, "myMethod");
console.log(metadata); // 输出: "someValue"

这里首先要了解下元数据和反射两个概念,他们都是与元编程相关的概念:

  1. 元数据(Metadata)

    • 元数据是有关数据的数据。它是一种描述数据的信息,通常用于描述数据的属性、类型、特征等。
    • 在编程中,元数据通常被用来描述类、方法、属性或其他程序实体的特征。这些信息可以包括文档、类型信息、注释、标签等。
    • 元数据在 JavaScript 中通常以注释、对象属性、特殊属性等形式存在,用于描述代码的各个方面。
  2. 反射(Reflection)

    • 反射是指在运行时检查和操作程序的结构、类型、属性和行为的能力。它允许程序动态地获取关于自身的信息并进行操作。
    • 在 JavaScript 中,反射可以通过内置的对象和方法来实现,如 Reflect 对象和一些特殊方法,比如 Object.keys()Object.getOwnPropertyNames()typeof 运算符等。
    • 反射允许你在运行时获取对象的属性、方法,检查对象的类型,动态创建对象,修改对象的属性等。它对于元编程和动态代码生成非常有用。

上面代码@Reflect.metadata("custom:annotation", "someValue"): 这是一个装饰器,它应用于 myMethod 方法。装饰器用于为方法添加元数据,其中 "custom:annotation" 是元数据的键,而 "someValue" 是元数据的值。这意味着在运行时,你可以使用反射来检索与 myMethod 方法相关的 "custom:annotation" 元数据,其值为 "someValue"

const metadata = Reflect.getMetadata("custom:annotation", MyClass.prototype, "myMethod");: 这行代码使用 Reflect.getMetadata 方法来获取指定元数据的值。具体来说,它尝试从 MyClass 类的原型对象(MyClass.prototype)中获取 myMethod 方法上的 "custom:annotation" 元数据的值,并将其存储在 metadata 变量中。

core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的装饰器:

  1. @autobind

    autobind装饰器使得方法中的this对象,绑定原始对象。

    typescript 复制代码
    import { autobind } from 'core-decorators';
    ​
    class Person {
      @autobind
      getPerson() {
        return this;
      }
    }
    ​
    let person = new Person();
    let getPerson = person.getPerson;
    ​
    getPerson() === person;
    // true
  2. @readonly

    readonly装饰器使得属性或方法不可写。

    ini 复制代码
    import { readonly } from 'core-decorators';
    ​
    class Meal {
      @readonly
      entree = 'steak';
    }
    ​
    var dinner = new Meal();
    dinner.entree = 'salmon';
    // Cannot assign to read only property 'entree' of [object Object]
  3. @override

    scala 复制代码
    import { override } from 'core-decorators';
    ​
    class Parent {
      speak(first, second) {}
    }
    ​
    class Child extends Parent {
      @override
      speak() {}
      // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
    }
    ​
    // or
    ​
    class Child extends Parent {
      @override
      speaks() {}
      // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
      //
      //   Did you mean "speak"?
    }
装饰器模式

装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许你在不改变对象自身的基础上,动态地添加行为或责任。这种模式通过创建一系列包装(装饰器)来扩展对象的功能。装饰器模式通常用于以下情况:

  1. 在不改变对象接口的情况下增加功能: 装饰器模式允许你添加新的功能,而无需修改现有对象的接口。这有助于保持代码的开放-封闭原则,即对扩展开放,对修改封闭。
  2. 动态组合功能: 你可以使用多个装饰器来组合不同的功能,以满足特定需求。这种组合是动态的,可以根据运行时需求进行更改。
  3. 避免子类爆炸: 装饰器模式可以用来替代创建大量子类的情况。相对于创建许多不同子类,你可以通过组合装饰器来实现不同的功能组合。

基本元素和角色在装饰器模式中包括:

  • Component(组件): 定义一个接口,所有具体组件和装饰器都实现这个接口。
  • Concrete Component(具体组件): 实现了 Component 接口的具体对象,它是被装饰的对象。
  • Decorator(装饰器): 也实现了 Component 接口,通常包含一个指向 Component 对象的引用,以便动态地添加责任。
  • Concrete Decorators(具体装饰器): 这些是扩展具体组件功能的装饰器。它们可以添加额外的行为或修改组件的行为。

下面是一个示例,演示了如何使用装饰器模式来扩展一个咖啡店的订单系统:

scala 复制代码
// Component
interface Coffee {
  cost(): number;
}
​
// Concrete Component
class SimpleCoffee implements Coffee {
  cost() {
    return 5;
  }
}
​
// Decorator
abstract class CoffeeDecorator implements Coffee {
  protected decoratedCoffee: Coffee;
​
  constructor(coffee: Coffee) {
    this.decoratedCoffee = coffee;
  }
​
  cost() {
    return this.decoratedCoffee.cost();
  }
}
​
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 2;
  }
}
​
class SugarDecorator extends CoffeeDecorator {
  cost() {
    return super.cost() + 1;
  }
}
​
// Usage
const coffee = new SimpleCoffee();
console.log(coffee.cost()); // 输出 5
​
const coffeeWithMilk = new MilkDecorator(coffee);
console.log(coffeeWithMilk.cost()); // 输出 7
​
const coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk);
console.log(coffeeWithMilkAndSugar.cost()); // 输出 8
​

这个示例中,我们有一个 Coffee 接口代表咖啡,一个 SimpleCoffee 类实现了这个接口。然后,我们创建了两个装饰器(MilkDecoratorSugarDecorator),分别添加了牛奶和糖的费用。最后,我们组合这些装饰器以创建一个具有不同功能的咖啡对象。这允许我们动态地添加和组合咖啡的功能,而不改变原始咖啡对象的接口。

TS装饰器和装饰器模式的区别

TypeScript 中的装饰器(Decorators)和设计模式中的装饰器模式虽然都涉及"装饰"这个词,但它们是不同的概念,具有不同的用途和实现方式。

  1. TypeScript 装饰器: TypeScript 装饰器是一种特殊的语法,用于在类、方法、属性等声明之前添加元数据或修改其行为。装饰器通常用于修改或扩展类的行为,例如添加日志、验证、路由信息等。装饰器是 TypeScript 的特性,用于在编译时修改类的结构或行为。它们可以用于各种用途,如 Angular 框架中的组件装饰器、Express.js 中的路由装饰器等。

    装饰器在 TypeScript 中使用 @ 符号,如下所示:

    ruby 复制代码
    class MyClass {
      // class implementation
    }
  2. 装饰器模式: 装饰器模式是一种设计模式,属于面向对象设计模式的一部分。它用于动态地添加责任或行为到对象,而不需要修改对象的代码。在装饰器模式中,有一个基本组件和一组装饰器,装饰器可以嵌套使用以增加对象的功能。这种模式用于扩展对象的功能,同时保持对象的接口不变。装饰器模式通常涉及创建一系列包装对象来动态地增加功能。

    举例来说,装饰器模式可以用于扩展一个文本编辑器的功能,如添加字体样式、颜色、下划线等,而不改变文本编辑器本身的接口。

总结: TypeScript 装饰器是一种编程语法,用于修改类、方法、属性等的行为和元数据,而装饰器模式是一种设计模式,用于动态地添加责任或行为到对象。它们的用途和实现方式不同,但都涉及在对象上添加功能,不过 TypeScript 装饰器更关注于编译时的元数据和行为修改,而装饰器模式更关注于运行时的功能组合。

相关推荐
一只特立独行的猪6111 小时前
Java面试——集合篇
java·开发语言·面试
让开,我要吃人了2 小时前
HarmonyOS开发实战(5.0)实现二楼上划进入首页效果详解
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙系统
everyStudy3 小时前
前端五种排序
前端·算法·排序算法
甜兒.4 小时前
鸿蒙小技巧
前端·华为·typescript·harmonyos
她似晚风般温柔7896 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
王中阳Go7 小时前
字节跳动的微服务独家面经
微服务·面试·golang
Jiaberrr8 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy8 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白8 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、8 小时前
Web Worker 简单使用
前端