TypeScript装饰器

358 阅读2分钟

一、简介

  1. 装饰器本质上是一种特殊的函数,使用 @expression 形式,可以对类、属性、方法和参数进行扩展,同时能让代码更加简洁。
  2. 装饰器有5种:类装饰器、属性装饰器、方法装饰器、参数装饰器、访问器装饰器

二、类装饰器

  1. 基本语法

类装饰器是应用在类声明上的函数,可以为类提供额外的功能,或者添加额外的逻辑。

function Demo(target: Function) {
    console.log(`Class name: ${target}`);
}
@Demo
class Person {
    constructor(public name: string, public age: number) {}
}

2. ### 应用举例

function CustomToString(target: Function) {
    target.prototype.toString = function() {
        return JSON.stringify(this);
    }
}
@CustomToString
class Person {
    constructor(public name: string, public age: number) {}
}

const p = new Person("John",12);
console.log(p.toString()); // Output: {"name":"John","age":12}

上面的例子中,我们为类Person定义了一个CustomToString装饰器,其功能是给Person的实例添加toString方法,并让其返回Person的实例的JSON.stringify后的结果。

  1. 关于返回值

  • 有返回值:若装饰器返回一个新的类,那这个新的类将会替换被装饰的类。
function Demo(target: Function) {
    return class {
        test() {
            console.log("welcom!");
        }
    }
}

@Demo
class Person {
    test() {
        console.log("Hello, World!");
    }
}

const p = new Person();
p.test(); // welcom

4. ### 关于构造类型

在typescript中,Function所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等。并非Function类型的方法都可以被new关键字实例化,例如箭头函数不能被实例化。那么typescript中如何声明构造器类型呢,以下有两种方法

  1. 仅声明构造器类型
type Constructor = new (...args: any[]) => {}

function test(fn: Constructor) {}

class Person {

}
test(Person)

2. 声明构造器类型+静态属性

type Constructor = {
    new (...args: any[]): {}
    wife: string
}

function test(fn: Constructor) {}

class Person {
    static wife: string 
}
test(Person)

5. ### 替换被装饰的类

设计一个logTime的装饰器,可以给实例添加一个属性,用于记录对象的创建时间,并且提供一个方法,返回对象的创建时间

type Constructor = new (...args: any[]) =>{}
interface Person {
    getCreateTime(): void
}
function LogTime<T extends Constructor>(target: T) {
    return class extends target {
        createTime: Date
        constructor(...args: any[]) {
            super(...args);
            this.createTime = new Date();
        }

        getCreateTime() {
            return `创建该对象的时间为${this.createTime}`;
        }
    }
}

@LogTime
class Person {
    constructor(public name: string, public age: number) {}
    speak(): void {
        console.log(`${this.name} is speaking...`);
    }
}

const p = new Person("John", 20);
console.log(p.getCreateTime()); // 创建该对象的时间为Thu Feb 11 2021 14:48:47 GMT+0800 (GMT+08:00)

三、装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,可以为装饰器传递参数,可以更灵活的控制装饰器的行为。

下面的例子定义了一个可以接收参数的装饰器工厂函数,在target的原型上添加了一个introduce方法,打印n词呢日志信息。

interface Person {
    introduce(): void
}

function logInfo(n: number) {
    return function(target: Function) {
        target.prototype.introduce = function() {
            for(let i = 0; i < n; i++) {
                console.log(`Hello, I am ${this.name}`);
            }
        }
    }
}

@logInfo(3)
class Person {
    constructor(public name: string, public age: number) {}
    speak() {
        console.log(`hello`);
    }
}

const p = new Person("John", 25);
p.introduce();

四、装饰器组合

装饰器组合可以组合使用,执行顺序是【由上到下】依次执行所有的装饰器工厂,获取到装饰器;在【由下到上】执行所有的装饰器

  1. 执行顺序

function test1(target: Function) {
    console.log('test1')
}
function test2() {
    console.log('test2 工厂')
    return function(target: Function) {
        console.log('test2')
    }
}
function test3() {
    console.log('test3 工厂')
    return function(target: Function) {
        console.log('test3')
    }
}
function test4(target: Function) {
    console.log('test4')
}
@test1
@test2()
@test3()
@test4
class Test {}

// 控制台打印的顺序是:
// test2 工厂
// test3 工厂
// test4
// test3
// test2
// test1

2. 应用

interface Person {
    introduce(): void
    getCreateTime(): void
}

function CustomToString(target: Function) {
    target.prototype.toString = function() {
        return JSON.stringify(this);
    }
}

type Constructor = new (...args: any[]) =>{}
function LogTime<T extends Constructor>(target: T) {
    return class extends target {
        createTime: Date
        constructor(...args: any[]) {
            super(...args);
            this.createTime = new Date();
        }

        getCreateTime() {
            return `创建该对象的时间为${this.createTime}`;
        }
    }
}

function logInfo(n: number) {
    return function(target: Function) {
        target.prototype.introduce = function() {
            for(let i = 0; i < n; i++) {
                console.log(`Hello, I am ${this.name}`);
            }
        }
    }
}

@CustomToString
@logInfo(3)
@LogTime
class Person {
    constructor(public name: string, public age: number) {}
}

const p = new Person("John",12);
console.log(p.toString()); // Output: {"name":"John","age":12}
console.log(p.getCreateTime()); // Output: 创建该对象的时间为Thu Dec 10 2021 19:42:57 GMT+0800 (China Standard Time)
p.introduce();

五、属性装饰器

属性装饰器(Property Decorator)是一种特殊的声明,它能够被附加到类的属性声明上,用于在运行时或编译时提供关于该属性的额外信息或修改其行为。属性装饰器在TypeScript中是通过@expression的语法形式来使用的,其中expression必须计算为一个函数,这个函数会在运行时被调用,并传入关于被装饰属性的元数据信息。

  1. 基本语法

/**
 * 参数说明:
 *  - target: 对于静态属性来说是类,对于实例属性来说是类的原型对象
 *  - propertyKey: 属性名
 * 
 */
function logPropertyName(target: object, propertyKey: string) {
    console.log(target)
    console.log(`Property ${propertyKey} has been decorated.`);
}

class MyClass {
   @logPropertyName myProperty: string;
   @logPropertyName static myStatic: string
    constructor(myProperty: string) { 
        this.myProperty = myProperty
    }
}

2. #### 关于属性遮蔽

如下代码:当构造器上的this.name = name试图在实例上赋值时,实际上是调用了原型上name属性的set方法。


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

let value = 'John';
Object.defineProperty(Person.prototype, "name", {
    get: function () {
        return value;
    },
    set(v) {
        value = v;
    },
})

const person1 = new Person("John Doe", 25);

console.log(person1.name); // John Doe
console.log(Person.prototype.name) // John Doe
  1. 应用举例

以下代码实现了一个State装饰器,其功能在更新属性时监听到了属性值的变化。

function State(target: object, propertyKey: string) {
    let key = `__${propertyKey}`
    Object.defineProperty(target, propertyKey, {
        get: function() {
            return this[key];
        },
        set: function(value) {
            console.log(`${propertyKey} 更新了`)
            this[key] = value;
        }
    });
}

class Person {
    @State name: string;
    @State age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

const person = new Person('John Doe', 30);
person.name = 'John';
person.age = 25;

六、方法装饰器

方法装饰器(Method Decorator)是一种允许你在类的方法上添加额外逻辑或行为的装饰器。方法装饰器接收三个参数:target(类的原型)、propertyName(方法名),以及descriptor(方法的描述符对象,它包含了方法的一些元数据,比如是否可枚举、可配置等,以及方法的实现)。

通过方法装饰器,你可以在方法被调用之前或之后执行额外的代码,比如进行日志记录、性能测量、权限检查、参数验证等。

  1. 基本语法


/**
 * 
 * @param target 对于静态方法来说是类,对于实例方法来说是原型对象
 * @param propertyKey 被装饰的方法名称
 * @param descriptor 被装饰的方法的描述对象
 */
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor): void{
   console.log('target:' ,target)
   console.log('propertyKey:', propertyKey)
   console.log('descriptor:', descriptor) 
}

class Person {
    constructor(public name: string, public age: number) {}
    @Demo
    speak() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
    @Demo
    static test() {
        console.log('This is a static method.');
    }
}

上面的代码运行结果如下:

  1. 应用举例

以下代码实现了一个方法装饰器Logger,功能是在被装饰方法执行前打印一段日志,执行后打印一段日志。


function Logger(target: object, prepertyKey: string, descriptor: PropertyDescriptor) {
    // 获取原始方法
    const originnal = descriptor.value;
    // 重写原始方法
    descriptor.value = function(...args: any[]) {
        console.log(`Before invoking ${prepertyKey} method...`);
        const result = originnal.call(this, ...args);
        console.log(`After invoking ${prepertyKey} method, result: ${result}`);
        return result;
    }
}

class Person {
    constructor(public name: string, public age: number) {}

    @Logger
    speak() {
        console.log(`${this.name} is speaking!`);
    }
    @Logger
    static isAduit(age: number): boolean {
        return age >= 18;
    }
}

const p = new Person('John', 20);
p.speak();
Person.isAduit(20);

控制台打印结果如下:

七、访问器装饰器

访问器装饰器(Accessor Decorator)是一种特殊的装饰器,它允许你在类的属性的getter或setter方法上添加额外的逻辑或行为。这种装饰器接收三个参数:target(类的原型)、propertyName(属性名)以及一个描述该属性是getter还是setter的descriptor对象。

访问器装饰器可以让你在访问或修改属性时执行额外的代码,比如日志记录、数据验证或转换等。

  1. 基本语法

function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target)
    console.log(propertyKey)
    console.log(descriptor)
}

class Person {
    constructor(public name: string, public age: number) {}
    
    @Demo
    get address() {
        return "东升科技园"
    }
    @Demo
    get country() {
        return "中国"
    }
}

2. 应用举例

function RangeValidate(min: number, max: number) {
    return function(target: object, property: string, descriptor: PropertyDescriptor) {        // 保存原始setter
        const originalSetter = descriptor.set
        // 重写原始setter
        descriptor.set = function(value: number) {
            if (value >= min && value <= max) {
                originalSetter && originalSetter.call(this, value)
            } else {
                throw new Error(`Temperature must be between ${min} and ${max} degrees Celsius`)
            }
        }
    }
}

class Weather {
    private _temp: number
    constructor(temp: number) {
        this._temp = temp
    }

    get temp() {
        return this._temp
    }
    @RangeValidate(-40, 40)
    set temp(value: number) {
        this._temp = value
    }
}

const w = new Weather(20)
w.temp = 30
w.temp = 100

八、参数装饰器

参数装饰器是一种特殊的装饰器,用于在类的方法或构造函数的参数被定义时进行额外的操作。参数装饰器接收三个参数:target(类的原型)、methodName(方法的名字)和paramIndex(参数的索引)。通过参数装饰器,你可以在参数被使用之前对其进行验证、转换或其他处理

  1. 基本语法


/**
 * 
 * @param target 如果修饰的是【实例方法】的参数,值是类的【原型对象】;如果修饰的是【静态方法】的参数,值是【类】
 * @param propertyKey 参数所在的方法名称
 * @param parameterIndex 参数所在函数参数列表中的索引
 */
function Demo(target: object, propertyKey: string, parameterIndex: number) {

}

class Person{
    constructor(public name:string){}
    speak(@Demo message:string) {
        console.log(`${this.name} says: ${message}`);
    }
}
  1. 应用举例

// 参数装饰器定义
function logParameter(target: any, methodName: string, paramIndex: number) {
    // 创建一个新的函数来替换原有的参数
    const originalMethod = target[methodName];
    target[methodName] = function (...args: any[]) {
        // 在这里可以对参数进行任何处理
        console.log(`Argument at index ${paramIndex} is: ${args[paramIndex]}`);
        
        // 调用原始方法
        return originalMethod.apply(this, args);
    };
}

// 使用参数装饰器的类
class Example {
    // 在参数上应用装饰器
    greet(@logParameter message: string): void {
        console.log(`Hello, ${message}!`);
    }
}

// 创建类的实例并调用方法
const example = new Example();
example.greet('world');

在这个例子中,logParameter是一个参数装饰器,它被应用于Example类的greet方法的message参数上。当greet方法被调用时,装饰器会首先执行,打印出参数的日志,然后才会执行原始的方法体。

输出将会是:

Argument at index 0 is: world
Hello, world!