likes
comments
collection
share

TypeScript 5.0 支持 Stage 3 阶段装饰器

作者站长头像
站长
· 阅读数 1

装饰器目前已经处于stage3阶段,虽然还未正式发布,但是已经稳定,预计很快就会发布。

先来看一段代码:

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调用来帮助调试。

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 的函数,如下所示:

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方法:

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中,通常会编写如下模式:

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声明为初始化为箭头函数的属性。

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    greet = () => {
        console.log(`Hello, my name is ${this.name}.`);
    };
}

编写此代码是为了确保在将greet作为独立函数调用或作为回调传递时不会重新绑定。

const greet = new Person("Ron").greet;

greet();

我们可以编写一个装饰器,使用addInitializer在构造函数中为我们调用bind。

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没有返回任何东西——所以当它修饰一个方法时,它保留了原来的方法。相应的,它会在初始化任何其他字段之前添加逻辑。

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))。如下:

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

同样值得注意的是:如果您更喜欢风格,您可以将这些装饰器放在同一行。

    @bound @loggedMethod greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }

我们甚至可以创建返回decorator函数的函数。这样就可以稍微定制一下最终的装饰器。如果我们愿意,我们可以让loggedMethod返回一个装饰器,并自定义它记录消息的方式。

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作为装饰器之前调用它。然后,我们可以传入任何字符串作为记录到控制台的消息的前缀。

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和自动访问器。

参考原文地址