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

参考原文地址

相关推荐
@@@@@@@@5415 小时前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
用户09340777351417 小时前
HarmonyOS WPS Open SDK 实践:最小打开闭环与 enableEdit 分支处理
typescript·harmonyos
柯克七七1 天前
我把 bug 修得太干净了,测试组以为这个模块本来就没问题
前端·javascript·typescript
退休倒计时1 天前
【每日一题】LeetCode 39. 组合总和 TypeScript
算法·leetcode·typescript
yyt3630458412 天前
KlineChartQuant Tooltip 高频交互 200FPS 性能优化完整复盘
前端·经验分享·性能优化·typescript·vue·交互·chrome devtools
用户0934077735142 天前
HarmonyOS WPS Open SDK 实践:办公审批附件「打开—编辑—回传」代码实现
typescript·harmonyos
Flynt2 天前
TypeScript 5.8 让我少踩了一个大坑,顺手再聊聊 --erasableSyntaxOnly
前端·javascript·typescript
退休倒计时3 天前
【每日一题】LeetCode 17. 电话号码的字母组合 TypeScript
算法·leetcode·typescript
无情的西瓜皮4 天前
MCP 协议实战:从零搭建一个 MCP 服务器(Node.js 版)
vue.js·人工智能·typescript·node.js
退休倒计时4 天前
【每日一题】LeetCode 78. 子集 TypeScript
算法·leetcode·typescript