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

参考原文地址

相关推荐
Dragon Wu21 小时前
Electron Forge集成React Typescript完整步骤
前端·javascript·react.js·typescript·electron·reactjs
We་ct1 天前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
程序猿阿伟2 天前
《TypeScript中Protobuf到运行时类型安全的转换指南》
javascript·安全·typescript
We་ct2 天前
LeetCode 228. 汇总区间:解题思路+代码详解
前端·算法·leetcode·typescript
阿蒙Amon2 天前
TypeScript学习-第10章:模块与命名空间
学习·ubuntu·typescript
VT.馒头3 天前
【力扣】2695. 包装数组
前端·javascript·算法·leetcode·职场和发展·typescript
AAA阿giao3 天前
从零拆解一个 React + TypeScript 的 TodoList:模块化、数据流与工程实践
前端·react.js·ui·typescript·前端框架
hedley(●'◡'●)3 天前
基于cesium和vue的大疆司空模仿程序
前端·javascript·vue.js·python·typescript·无人机
百锦再3 天前
Vue高阶知识:利用 defineModel 特性开发搜索组件组合
前端·vue.js·学习·flutter·typescript·前端框架
小杨同学呀呀呀呀3 天前
Ant Design Vue <a-timeline>时间轴组件失效解决方案
前端·javascript·vue.js·typescript·anti-design-vue