随着 TypeScript 和 ES6 中类的引入,现在存在某些场景需要额外的功能来支持标注或修改类和类成员。 装饰器提供了一种为类声明和成员添加标注和元编程语法的方法。接下来记录下装饰器的学习。
环境搭建
安装ts-node
,这个工具可以直接执行 TypeScript 文件,而不需要将其编译为 JavaScript 文件。
然后运行tsc --init
生成tsconfig.json
,tsconfig.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
作为参数,并返回一个经过装饰的新类。
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T)
:这是函数的签名,它定义了一个泛型函数classDecorator
,接受一个构造函数constructor
作为参数。这个构造函数必须满足一个条件,即必须具有一个可以接受任意参数的构造函数,因为它应该是一个类的构造函数。return class extends constructor { ... }
:这部分是函数的主体,它返回一个新的类,这个类是通过继承传入的constructor
构造函数创建的。在这个新的类内部,可以添加新的属性和方法,或者重写原始类的属性和方法。newProperty = "new property";
:这行代码在新的类中添加了一个名为newProperty
的属性,并给它赋了初始值 "new property"。hello = "override";
:这行代码重写了原始类的属性hello
,并将其值设置为 "override"。
总结一下,这个装饰器函数的作用是创建一个新的类,该类继承自传入的构造函数(原始类),并在新类中添加了新的属性 newProperty
,以及重写了原始类的属性 hello
。当你应用这个装饰器函数时,原始类的实例将拥有新类的属性和方法,同时保留了原始类的功能。这允许你在不修改原始类的情况下扩展其功能或添加新的
方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器接受三个参数:
target
:表示被装饰方法所属的类的构造函数。对于实例方法,target
是类的构造函数;对于静态方法,target
是类本身。propertyKey
:表示被装饰的方法的名称。对于实例方法,propertyKey
是方法的名称;对于静态方法,也是方法的名称。descriptor
:是一个包含了被装饰方法属性描述的对象。它通常包括value
,writable
,enumerable
和configurable
等属性。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?
可以理解为方法装饰器返回了函数时,就是替换了原来的方法。
访问器装饰器
访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义。
访问器属性接受三个参数:
target
:表示被装饰的类的原型(对于实例成员)或构造函数本身(对于静态成员)。propertyKey
:表示被装饰的成员的名称,通常是一个字符串,对于实例成员就是类的原型上的属性名称,对于静态成员就是类本身的属性名称。descriptor
:是一个包含了被装饰成员属性描述的对象。这个descriptor
对象通常包括get
和set
方法,允许你访问和修改访问器的行为。
看这个例子:
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
成员变量装饰器
也可以称之为属性装饰器,接受两个参数:
target
:表示被装饰的类的原型(对于实例属性)或构造函数本身(对于静态属性)。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); // 输出并调用装饰器中的逻辑
参数装饰器
参数装饰器接受三个参数:
target
:表示被装饰的方法的类的原型(对于实例方法)或构造函数本身(对于静态方法)。methodName
:表示被装饰的方法的名称。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:
Reflect.defineMetadata(key, value, target, propertyKey)
:用于在指定的目标上定义元数据。key
是元数据的键,value
是元数据的值,target
表示要添加元数据的目标,propertyKey
表示目标上的属性或方法的名称。Reflect.getMetadata(key, target, propertyKey)
:用于从指定的目标上获取元数据。key
是元数据的键,target
表示要获取元数据的目标,propertyKey
表示目标上的属性或方法的名称。Reflect.hasMetadata(key, target, propertyKey)
:用于检查指定的目标是否包含特定键的元数据。key
是元数据的键,target
表示要检查的目标,propertyKey
表示目标上的属性或方法的名称。Reflect.metadata(key, value)
:这是一个装饰器工厂函数,用于将元数据添加到类的属性或方法上。你可以在类成员上使用@Reflect.metadata(key, value)
装饰器来定义元数据。
在 TypeScript 中,要使用 reflect-metadata
特性,你需要确保在 tsconfig.json
中开启 experimentalDecorators
和 emitDecoratorMetadata
这两个编译器选项:
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"
这里首先要了解下元数据和反射两个概念,他们都是与元编程相关的概念:
-
元数据(Metadata) :
- 元数据是有关数据的数据。它是一种描述数据的信息,通常用于描述数据的属性、类型、特征等。
- 在编程中,元数据通常被用来描述类、方法、属性或其他程序实体的特征。这些信息可以包括文档、类型信息、注释、标签等。
- 元数据在 JavaScript 中通常以注释、对象属性、特殊属性等形式存在,用于描述代码的各个方面。
-
反射(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是一个第三方模块,提供了几个常见的装饰器:
-
@autobind
autobind
装饰器使得方法中的this
对象,绑定原始对象。typescriptimport { autobind } from 'core-decorators'; class Person { @autobind getPerson() { return this; } } let person = new Person(); let getPerson = person.getPerson; getPerson() === person; // true
-
@readonly
readonly
装饰器使得属性或方法不可写。iniimport { 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]
-
@override
scalaimport { 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)是一种结构型设计模式,它允许你在不改变对象自身的基础上,动态地添加行为或责任。这种模式通过创建一系列包装(装饰器)来扩展对象的功能。装饰器模式通常用于以下情况:
- 在不改变对象接口的情况下增加功能: 装饰器模式允许你添加新的功能,而无需修改现有对象的接口。这有助于保持代码的开放-封闭原则,即对扩展开放,对修改封闭。
- 动态组合功能: 你可以使用多个装饰器来组合不同的功能,以满足特定需求。这种组合是动态的,可以根据运行时需求进行更改。
- 避免子类爆炸: 装饰器模式可以用来替代创建大量子类的情况。相对于创建许多不同子类,你可以通过组合装饰器来实现不同的功能组合。
基本元素和角色在装饰器模式中包括:
- 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
类实现了这个接口。然后,我们创建了两个装饰器(MilkDecorator
和 SugarDecorator
),分别添加了牛奶和糖的费用。最后,我们组合这些装饰器以创建一个具有不同功能的咖啡对象。这允许我们动态地添加和组合咖啡的功能,而不改变原始咖啡对象的接口。
TS装饰器和装饰器模式的区别
TypeScript 中的装饰器(Decorators)和设计模式中的装饰器模式虽然都涉及"装饰"这个词,但它们是不同的概念,具有不同的用途和实现方式。
-
TypeScript 装饰器: TypeScript 装饰器是一种特殊的语法,用于在类、方法、属性等声明之前添加元数据或修改其行为。装饰器通常用于修改或扩展类的行为,例如添加日志、验证、路由信息等。装饰器是 TypeScript 的特性,用于在编译时修改类的结构或行为。它们可以用于各种用途,如 Angular 框架中的组件装饰器、Express.js 中的路由装饰器等。
装饰器在 TypeScript 中使用
@
符号,如下所示:rubyclass MyClass { // class implementation }
-
装饰器模式: 装饰器模式是一种设计模式,属于面向对象设计模式的一部分。它用于动态地添加责任或行为到对象,而不需要修改对象的代码。在装饰器模式中,有一个基本组件和一组装饰器,装饰器可以嵌套使用以增加对象的功能。这种模式用于扩展对象的功能,同时保持对象的接口不变。装饰器模式通常涉及创建一系列包装对象来动态地增加功能。
举例来说,装饰器模式可以用于扩展一个文本编辑器的功能,如添加字体样式、颜色、下划线等,而不改变文本编辑器本身的接口。
总结: TypeScript 装饰器是一种编程语法,用于修改类、方法、属性等的行为和元数据,而装饰器模式是一种设计模式,用于动态地添加责任或行为到对象。它们的用途和实现方式不同,但都涉及在对象上添加功能,不过 TypeScript 装饰器更关注于编译时的元数据和行为修改,而装饰器模式更关注于运行时的功能组合。