TypeScript 5.0 支持 Stage 3 阶段装饰器

装饰器目前已经处于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和自动访问器。

参考原文地址

相关推荐
清灵xmf3 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
Amd79411 小时前
Nuxt.js 应用中的 prepare:types 事件钩子详解
typescript·自定义·配置·nuxt·构建·钩子·类型
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
鸿蒙开天组●2 天前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70
zhizhiqiuya2 天前
第二章 TypeScript 函数详解
前端·javascript·typescript
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
_jiang2 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
清清ww2 天前
【TS】九天学会TS语法---计划篇
前端·typescript
努力变厉害的小超超3 天前
TypeScript中的类型注解、Interface接口、泛型
javascript·typescript