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 装饰器更关注于编译时的元数据和行为修改,而装饰器模式更关注于运行时的功能组合。

相关推荐
你挚爱的强哥28 分钟前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森1 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js