装饰器目前已经处于stage3阶段,虽然还未正式发布,但是已经稳定,预计很快就会发布。
先来看一段代码:
typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
Greet在这里非常简单,但实际开发中可能会涉及诸如异步,递归等操作,假设在这里引入了一些console.log调用来帮助调试。
typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}
装饰器可以为多个方法添加相同的操作。我们可以编写一个名为 LoggedMethod 的函数,如下所示:
typescript
function loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
// Output:
//
// LOG: Entering method.
// Hello, my name is Ron.
// LOG: Exiting method.
使用loggedMethod作为greet上面的装饰器,注意这里写成了@loggedMethod。当我们那样做时,它被用target方法和一个context对象调用。因为loggedMethod返回了一个新函数,所以该函数取代了greet的原始定义。我们还没有提到,loggedMethod第二个参数被称为"上下文对象",它有一些关于修饰方法是如何声明的有用信息------比如它是一个#private成员,还是静态的,或者方法的名字是什么。利用context重写loggedMethod方法:
typescript
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
TypeScript提供了一个名为ClassMethodDecoratorContext的类型,定义方法装饰器使用的上下文对象。 除了元数据之外,方法的上下文对象还有一个名为addInitializer的有用函数。addInitializer
方法是一个 TypeScript 编译器 API,用于向装饰器声明中添加初始化器。它允许在实例化类时执行一些初始化逻辑。这个方法是 TypeScript 编译器 API 的一部分,用于增强类型检查和类型推断。
通常情况下,addInitializer
方法在编译器在处理装饰器声明时被调用,这意味着它的执行时机是在 TypeScript 编译阶段而非运行时。具体来说,它在装饰器被应用到类上时被调用,用于修改类的元数据,例如在类的构造函数上添加一些逻辑。 举个例子,在JavaScript中,通常会编写如下模式:
typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
或者,可以将greet声明为初始化为箭头函数的属性。
ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet = () => {
console.log(`Hello, my name is ${this.name}.`);
};
}
编写此代码是为了确保在将greet作为独立函数调用或作为回调传递时不会重新绑定。
ts
const greet = new Person("Ron").greet;
greet();
我们可以编写一个装饰器,使用addInitializer在构造函数中为我们调用bind。
javascript
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
if (context.private) {
throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}
Bound没有返回任何东西------所以当它修饰一个方法时,它保留了原来的方法。相应的,它会在初始化任何其他字段之前添加逻辑。
typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
const greet = p.greet;
greet();
注意当多个装饰器应用于单个声明时,它们的评估类似于 数学中的函数组合。在此模型中,当复合函数 f 和 g 时,得到的复合 (f ∘ g)(x) 等效于 f(g(x))。如下:
less
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
//Output:
//
//first(): factory evaluated
//second(): factory evaluated
//second(): called
//first(): called
同样值得注意的是:如果您更喜欢风格,您可以将这些装饰器放在同一行。
less
@bound @loggedMethod greet() {
console.log(`Hello, my name is ${this.name}.`);
}
我们甚至可以创建返回decorator函数的函数。这样就可以稍微定制一下最终的装饰器。如果我们愿意,我们可以让loggedMethod返回一个装饰器,并自定义它记录消息的方式。
javascript
function loggedMethod(headMessage = "LOG:") {
return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`${headMessage} Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`${headMessage} Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
}
如果使用这种方式我们就必须在使用loggedMethod作为装饰器之前调用它。然后,我们可以传入任何字符串作为记录到控制台的消息的前缀。
typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod("#")
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
// Output:
//
// #Entering method 'greet'.
// Hello, my name is Ron.
// #Exiting method 'greet'.
装饰器不仅仅可以用在方法上。它们可用于属性/字段、getter、setter和自动访问器。