TS@装饰器使用和原理

88 阅读5分钟

1、装饰器

定义:在不修改原代码的情况下,通过注解方式为类、方法、属性或参数添加额外功能的特性

1.1调用方式

@expression这种形式,expression求值后必须为一个函数,在被调用时会调用表达式表示的函数

function firstDec (){
    console.log('firstDec')
}
function secondDec (){
    return function (){
        console.log('secondDec')
    }
}
class Greeter {
    @firstDec
    firstGreet(name: string) {
    }
    @secondDec()
    secondGreet(name: string) {
    }
}

new Greeter()
//firstDec
//secondDec

1.2装饰器组合

多个装饰器可以同时应用到一个声明上,多装饰器使用遵循以下规则

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用。
// 书写在同一行上
@f @g x

// 多行使用
@f
@g
x

2、装饰器工厂

装饰器工厂主要的应用场景就是让装饰器接收配置参数,实现动态生成返回。

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

3、装饰器执行顺序

  1. 参数装饰器会在方法装饰器之前执行,然后方法装饰器,访问符装饰器,或属性装饰器按书写顺序执行,最后应用到每个实例成员。
  2. 参数装饰器会在方法装饰器之前执行,然后方法装饰器,访问符装饰器,或属性装饰器按书写顺序执行,最后应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。
function ParamDecorator(target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
    console.log(`[参数装饰器] 目标: ${target?.constructor?.name || '构造函数'}, 属性: ${String(propertyKey || 'constructor')}, 参数索引: ${parameterIndex}`);
}

// 方法装饰器
function MethodDecorator(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log(`[方法装饰器] 目标: ${target.constructor.name}, 方法: ${String(propertyKey)}`);
    return descriptor;
}

// 访问符装饰器(getter/setter)
function AccessorDecorator(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log(`[访问符装饰器] 目标: ${target.constructor.name}, 访问符: ${String(propertyKey)}`);
    return descriptor;
}

// 属性装饰器
function PropertyDecorator(target: any, propertyKey: string | symbol) {
    console.log(`[属性装饰器] 目标: ${target.constructor.name}, 属性: ${String(propertyKey)}`);
}

// 类装饰器
function ClassDecorator(constructor: Function) {
    console.log(`[类装饰器] 类名: ${constructor.name}`);
}

@ClassDecorator
class Example {
    // 实例方法(带参数装饰器)
    @MethodDecorator
    instanceMethod(
        @ParamDecorator param1: string,
        @ParamDecorator param2: number
    ) {
        console.log('执行实例方法');
    }
    
    // 实例属性
    @PropertyDecorator
    instanceProperty: string = '实例属性';
    
    
    // 实例访问符(getter)
    @AccessorDecorator
    get instanceAccessor(): string {
        return this.instanceProperty;
    }

    // 实例访问符(setter)
    @AccessorDecorator
    set instanceAccessor(value: string) {
        this.instanceProperty = value;
    }

    // 静态属性
    @PropertyDecorator
    static staticProperty: string = '静态属性';

    // 静态方法(带参数装饰器)
    @MethodDecorator
    static staticMethod(
        @ParamDecorator param1: string,
        @ParamDecorator param2: number
    ) {
        console.log('执行静态方法');
    }

    // 静态访问符(getter)
    @AccessorDecorator
    static get staticAccessor(): string {
        return Example.staticProperty;
    }

    // 静态访问符(setter)
    @AccessorDecorator
    static set staticAccessor(value: string) {
        Example.staticProperty = value;
    }

    // 构造函数(带参数装饰器)
    constructor(
        @ParamDecorator param1: string,
        @ParamDecorator param2: number
    ) {
        console.log('构造函数执行');
    }
}

const example = new Example('test', 123);
//[参数装饰器] 目标: Example, 属性: instanceMethod, 参数索引: 1
//[参数装饰器] 目标: Example, 属性: instanceMethod, 参数索引: 0
//[方法装饰器] 目标: Example, 方法: instanceMethod
//[属性装饰器] 目标: Example, 属性: instanceProperty
//[访问符装饰器] 目标: Example, 访问符: instanceAccessor
//[访问符装饰器] 目标: Example, 访问符: instanceAccessor
//[属性装饰器] 目标: Function, 属性: staticProperty
//[参数装饰器] 目标: Function, 属性: staticMethod, 参数索引: 1
//[参数装饰器] 目标: Function, 属性: staticMethod, 参数索引: 0
//[方法装饰器] 目标: Function, 方法: staticMethod
//[访问符装饰器] 目标: Function, 访问符: staticAccessor
//[访问符装饰器] 目标: Function, 访问符: staticAccessor
//[参数装饰器] 目标: Function, 属性: constructor, 参数索引: 1
//[参数装饰器] 目标: Function, 属性: constructor, 参数索引: 0
//[类装饰器] 类名: Example
//构造函数执行

4、装饰器分类

4.1类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}
@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

类装饰器表达式会在运行时当作函数直接调用,类的构造函数作为其唯一的参数。 如果装饰器执行有返回,且是一个函数,则会用这个函数替换构造函数,返回其他类型值会直接报错

function firstDec (){
}
function secondDec (){
    return function Test (){
    }
}

@firstDec
class Greeter1 {
}

@secondDec
class Greeter2{
}

console.log(new Greeter1())
//Greeter1 {}
console.log(new Greeter2())
//Test {}

4.2方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。

function ReplaceMethod(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    console.log(`[替换装饰器] 替换方法: ${String(propertyKey)}`);

    // 返回一个新的方法实现
    return {
        ...descriptor,
        value: function (...args: any[]) {
            console.log(`方法 ${String(propertyKey)} 已被替换!`);
            console.log(`原始参数:`, args);
            return `替换后的返回值 - 参数: ${args.join(', ')}`;
        }
    };
}

class Example1 {
    @ReplaceMethod
    originalMethod(name: string, age: number): string {
        return `原始方法: ${name}, ${age}`;
    }
}

const ex1 = new Example1();
console.log(ex1.originalMethod('张三', 25));
// 输出: 方法 originalMethod 已被替换!
//      原始参数: [ '张三', 25 ]
//      替换后的返回值 - 参数: 张三, 25

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

注意  如果代码输出目标版本小于ES5Property Descriptor将会是undefined

有时你可以用一些其他的技巧,来对原方法进行扩展

function LogMethod(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value; // 保存原始方法

    // 返回包装后的方法
    return {
        ...descriptor,
        value: function (...args: any[]) {
            console.log(`[日志] 调用方法: ${String(propertyKey)}`);
            console.log(`[日志] 参数:`, args);

            const startTime = Date.now();
            const result = originalMethod.apply(this, args); // 调用原始方法
            const endTime = Date.now();

            console.log(`[日志] 返回值:`, result);
            console.log(`[日志] 执行时间: ${endTime - startTime}ms`);

            return result;
        }
    };
}

class Example2 {
    @LogMethod
    calculate(a: number, b: number): number {
        let sum = 0;
        for (let i = 0; i < 100; i++) {
            sum += i;
        }
        return a + b;
    }
}

const ex2 = new Example2();
console.log(ex2.calculate(10, 20));

4.3访问装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

注意  如果代码输出目标版本小于ES5Property Descriptor将会是undefined

function logAccess(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalGet = descriptor.get;
    const originalSet = descriptor.set;

    if (originalGet) {
        descriptor.get = function () {
            console.log(`访问 getter: ${propertyKey}`);
            return descriptor.value;
        };
    }

    if (originalSet) {
        descriptor.set = function (value: any) {
            console.log(`访问 setter: ${propertyKey}, 值: ${value}`);
            descriptor.value = value;
        };
    }
}

class Example {

    set val(value: string) {
    }

    @logAccess
    get val() {
        return this.val
    }
}

const ex = new Example();
ex.val = 'hello';
console.log(ex.val);
//访问 setter: val, 值: hello
//访问 getter: val
//hello

4.4属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
// 属性装饰器:标记属性为只读
function readonly(target: any, propertyKey: string) {
    const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {};
    descriptor.writable = false;
    Object.defineProperty(target, propertyKey, descriptor);
}

class User {
    @readonly
    name: string = '张三';
}

const user = new User();
console.log(user.name); // 张三
// user.name = '李四'; // 错误:无法赋值给只读属性

4.5参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。
function logParam(target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
    console.log(`参数装饰器: 方法 ${String(propertyKey)} 的第 ${parameterIndex} 个参数被标记`);
}

class Calculator {
    add(@logParam a: number, @logParam b: number): number {
        return a + b;
    }
}

const calc = new Calculator();
console.log(calc.add(5, 3)); // 8

5、原理

装饰器本质就是语法糖,在编译时转换为普通函数调用而已

装饰器原理

参考链接

TypeScript中文网