Typescript 高级语法 装饰器

216 阅读6分钟

什么是装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑。 个人认为装饰器是一种解决方案,而并非是狭义的@Decorator,后者仅仅是一个语法糖罢了。

在TypeScript中装饰器还属于实验性语法,你必须在命令行或tsconfig.json里启用
"experimentalDecorators": true,
"emitDecoratorMetadata": true,

总结

  • 装饰器本身是一个函数
  • 装饰器对类,方法,属性等等都可以进行装饰
  • 装饰器通过@符号进行引用

装饰器执行时机

修饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

在Node.js环境中模块一加载时就会执行

装饰器类型

方法装饰器

原型方法装饰器

// target 是 Test 的原型 类似于 Test.prototype
// propertyKey 是 方法的名字 这里面是 getName
// descriptor 是 属性描述符 
// 属性描述符是对JavaScript属性的描述,包括:value、writable、enumerable、configurable,除value其他默认为true
function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target);
    console.log("prop " + propertyKey);
    console.log("desc " + JSON.stringify(descriptor) + "\n\n");
};
function method2(flag) {
    // 这里面可以做一些逻辑处理 判断 flag 做一些处理判断
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log(target);
        console.log("prop " + propertyKey);
        console.log("desc " + JSON.stringify(descriptor) + "\n\n");
    };
};

class Test {
    // 原型方法 是挂载到 Test.prototype 上的
    @method
    @method2(true)
    getName() {
        return 'name';
    }
}

执行结果

// @method getName 输出
Test { getName: [Function] }
prop getName
desc {"writable":true,"enumerable":true,"configurable":true}

// @method2 getName 输出
Test { getName: [Function] }
prop getName
desc {"writable":true,"enumerable":true,"configurable":true}

静态方法装饰器

// target 是 Test 类,可以访问到 Test 类上的静态属性和静态方法
// propertyKey 是 方法的名字 这里面是 getAge
// descriptor 是 属性描述符 
// 属性描述符是对JavaScript属性的描述,包括:value、writable、enumerable、configurable,除value其他默认为true
function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target);
    console.log("prop " + propertyKey);
    console.log("desc " + JSON.stringify(descriptor) + "\n\n");
};

class Test {
    @method
    static getAge() {
        return 20;
    }
}

执行结果

{ [Function: Test] getAge: [Function] }
prop getAge
desc {"writable":true,"enumerable":true,"configurable":true}

属性装饰器

普通属性装饰器

// target 是 Test 的原型 类似于 Test.prototype
// propertyKey 是 属性的名字 这里面是 age
function attr(target: any, propertyKey: string) {
    console.log(target);
    console.log("prop " + propertyKey);
};

class Test {
    @attr
    age = 20;
}

执行结果

Test {}
prop age

静态属性装饰器

// target 是 Test 类,可以访问到 Test 类上的静态属性和静态方法
// propertyKey 是 属性的名字 这里面是 age
function attr(target: any, propertyKey: string) {
    console.log(target);
    console.log("prop " + propertyKey);
};

class Test {
    @attr
    static age = 30;
}

执行结果

{ [Function: Test] age: 30 }
prop age

方法参数装饰器

// target 这个参数根据方法是原型方法和静态方法有区分,如上类同
// 原型方法 => target 是 Test 的原型 类似于 Test.prototype
// 静态方法 => target 是 Test 类,可以访问到 Test 类上的静态属性和静态方法
// propertyKey 是 方法的名字 这里面是 getName 和 getAge
// propertyIndex 是方法里面参数的下标值,如 装饰器在第一个参数 propertyIndex 为0  在第二个参数 propertyIndex 为 1
function methodParams(target: any, propertyKey: string, propertyIndex: number) {
    console.log(target);
    console.log("prop " + propertyKey);
    console.log("index " + propertyIndex);
};

class Test {
    getName(@methodParams name: string, name2: string): string {
        return name;
    }
    getName2(name: string, @methodParams name2: string): string {
        return name;
    }
    static getAge(@methodParams name: string): string {
        return name;
    }
}

执行结果

Test { getName: [Function], getName2: [Function] }
prop getName
index 0  // 这个地方 index 值 为0

Test { getName: [Function], getName2: [Function] }
prop getName2
index 1  // 这个地方 index 值 为1

{ [Function: Test] getAge: [Function] }
prop getAge
index 0

访问器装饰器

// target 是 Test 的原型 类似于 Test.prototype
// propertyKey 是 方法的名字 这里面是 getName
// descriptor 是 属性描述符 
// 属性描述符是对JavaScript属性的描述,包括:value、writable、enumerable、configurable,除value其他默认为true
function decorat(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target);
    console.log("prop " + propertyKey);
    console.log("descriptor " + JSON.stringify(descriptor));
};

class Test {
    _name = 'li';
    
    // 访问器 name 属性 是在 Test.prototype 上的
    @decorat
    get name() {
        return this._name;
    }

    set name(name) {
        this._name = name;
    }
}

执行结果

Test { name: [Getter/Setter] }
prop name
descriptor {"enumerable":true,"configurable":true}

注意:访问器装饰器 只能在 get 或者 set 上使用,不能同时挂载装饰器,就算是挂载不同的装饰器也不行

类的装饰器

// 装饰器就是一个方法,所以定义一个方法,就是定义一个装饰器
// 类装饰器的方法的入参 constructor 是调用这个装饰器的类的构造函数
function TestDecorator(constructor: any) {
    console.log(constructor)
    constructor.prototype.getName = function() {
        console.log('name');
    }
}

// 装饰器的使用方法是 @[定义的装饰器] 
@TestDecorator
class Test {}
const test = new Test();
// 波浪线提示错误 --> 类型“Test”上不存在属性“getName”。 
test.getName(); // 不能正常执行
// 类型断言 去掉波浪线报错
(test as any).getName(); // 可以正常执行 控制台输出 name 

类上面可以修饰多个装饰器

@TestDecorator
@TestDecorator1
class Test {}

调用顺序是 TestDecorator1 --> TestDecorator

装饰器可以工厂模式返回一个方法

// flag 可以用来判断 返回不同的装饰器
function TestDecorator2(flag: boolean) {
    if(flag) {
        return function(constructor: any) {
            constructor.prototype.getName = function() {
                console.log('name');
            }
        }
    } else {
        return function(constructor: any) {}
    }
}

// 
@TestDecorator2(false)
class Test {}
const test = new Test();
// 类型断言
(test as any).getName()

看到这发现上面的代码有几个问题,constructor 定义为 any 类型,调用 getName 方法,必须要用类型断言

function TestDecorator() {
    // (...args: any[]) => any 这是一个函数 返回 any 类型
    // 函数接受很多参数,合并到args 数组中去,参数是any类型
    // new (...args: any[]) => any 是构造函数, T 继承了构造函数,那 constructor 就是也构造函数
    return function <T extends new (...args: any[]) => any>(constructor: T) {
        // 
        return class extends constructor {
            getName() {
                console.log('name');
            }
        }
    }
}

// TestDecorator() 返回了装饰器,把 class {} 作为入参传递进去。 constructor 就是 class {}
// 
const Test = TestDecorator()(
    class {
        
    }
)
const test2 = new Test2();
// 能访问到 getName 了
test2.getName();

案例实践

// 统一捕获异常装饰器
function dector(name: string) {
    return function(target: any, key: string, descriptor: PropertyDescriptor) {
        // 获取原方法
        const fn = descriptor.value;
        // 重新定义方法,把原方法包裹在 try catch 中
        descriptor.value = function() {
            try {
                fn();
            } catch(e) {
                console.log(name + ' 方法 发生异常了');
            }
        }
    }
}

const userInfo: any = undefined;

class Test {
    @dector('getName')
    getName() {
        return userInfo.name;
    }

    @dector('getAge')
    getAge() {
        return userInfo.age;
    }
    
    // 如果不加装饰器,必须要在每个方法里面捕获异常,有了装饰器就很简洁,方便
    // getHobby() {
    //     try {
    //         return userInfo.hobby    
    //     } catch (e) {
    //         console.log('getHobby 方法 发生异常了')
    //     }
    // }
}

const test = new Test();
test.getName();
test.getAge();

执行结果

getName 方法 发生异常了
getAge 方法 发生异常了