Typescript装饰器详解

时间:2023-10-24    作者:虣虣    分类: CocosCreator


Typescript装饰器详解

什么是装饰器(Decorator)

装饰器是ES7的一种新语法,他能够被附加到类、方法、访问器、属性或参数上。根据添加到不动的地方的装饰器有不同的名称和特性。

  • 附加到类上, 即类装饰器
  • 附加到方法上, 即方法装饰器
  • 附加到访问器上, 即访问器装饰器
  • 附加到属性上, 即属性装饰器
  • 附加到参数上, 即参数装饰器

装饰器的基本形式

  • 普通装饰器

在定义类前执行,并且执行的时候会把这个类传给装饰器

function testNormal(target) {
    console.log("普通装饰器!!!")
}
  • 装饰器工厂

在绑定的时候由于在函数后面写上了(),所以先会执行装饰器工厂拿到真正的装饰器,真正的装饰器会在类定义前执行

function testFactory() {// 装饰器工厂
    console.log("testFactory out")
    return (target) => { // 装饰器
        console.log("testFactory in")
    }
}
  • 组合装饰器

普通装饰器和装饰器工厂可以结合起来一起使用,会按到从上至下的顺序先执行装饰器工厂拿到真正的装饰器后再从下至上执行所有的装饰器。

function testNormal1(target) {
    console.log("普通装饰器1!!!")
}

function testNormal2(target) {
    console.log("普通装饰器2!!!")
}

function testFactory1() {// 装饰器工厂
    console.log("testFactory1 out")
    return (target) => { // 装饰器
        console.log("testFactory1 in")
    }
}

function testFactory2() {// 装饰器工厂
    console.log("testFactory2 out")
    return (target) => { // 装饰器
        console.log("testFactory2 in")
    }
}

@testNormal1
@testFactory1()
@testFactory2()
@testNormal2
class Person {
    constructor() {
        console.log(`Person 构造方法`)
    }
}
let a = new Person();

// output
testFactory1 out
testFactory2 out
普通装饰器 222 !!!
testFactory2 in
testFactory1 in
普通装饰器 111 !!!
Person 构造方法
  • 装饰器和装饰器工厂的区别

装饰器工厂和装饰器的区别在于装饰器工厂可以自由传递参数

类装饰器

类装装饰器不能用在声明文件(.d.ts)。类装饰器可以监听,修改,替换类的定义,在执行类装饰器函数时,会把绑定的类座位唯一的参数传递给装饰器。如果返回的有新的类型,则会替换原有的类

注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中不会为你做这些

// 普通类装饰器
function testClass(target) {
    return class extends target {
        name: string = "小明";
        age: number = 10;
    }
}
// 类装饰器工厂
function testClassFactory(parm1: string, parm2: number) {
    console.log("testClassFactory out");
    return (target) => {
        console.log("testClassFactory in");
        return class extends target {
            name: string = parm1;
            age: number = parm2;
        }
    }
}

@testClass
class Person21 {}

@testClassFactory("小红", 20)
class Person22 {}

let p21 = new Person21();
let p22 = new Person22();
console.log(p21);
console.log(p22);

// output
testClassFactory out
testClassFactory in
class_1 { name: '小明', age: 10 }
class_2 { name: '小红', age: 20 }

方法装饰器

方法装饰器不能用在声明文件(.d.ts),方法装饰器在运行时当做函数调用,有3个参数

  • 参数1: 静态方法: 类, 实例方法: 当前实例
  • 参数2: 被绑定方法的名字
  • 参数3: 被绑定的方法的属性描述符
// 普通方法装饰器
function testMothed(target: any, mothedName: string, descriptor: PropertyDescriptor) {
    console.log(`${mothedName} target:` + target)
    descriptor.value = (): void => {
        console.log(`${mothedName} new`);
    }
}
// 方法装饰器工厂
function testMothedFactory(parm1: string, parm2: number) {
    return (target: any, mothedName: string, descriptor: PropertyDescriptor) => {
        console.log(`${mothedName} target:` + target)
        descriptor.value = (): void => {
            console.log(`${mothedName} new, parm1: ${parm1} parm2: ${parm2}`);
        }
    }
}
class Person3 {
    @testMothed
    sayName(): void {
        console.log("sayName orgin");
    }

    @testMothed
    static sayStaticName(): void {
        console.log("sayName orgin");
    }

    @testMothedFactory("小红", 20)
    sayAge(): void {
        console.log("sayAge orgin");
    }
}

let p3 = new Person3();
p3.sayName();
p3.sayAge();
Person3.sayStaticName();

// output
sayName target:[object Object]
sayAge target:[object Object]
sayStaticName target:function Person3() {}
sayName new
sayAge new, parm1: 小红 parm2: 20
sayStaticName new

访问器装饰器

访问器装饰器不能用在声明文件(.d.ts),访问器装饰器在运行时会当做函数调用,传入3个参数

  • 参数1: 静态成员变量: 类的构造函数, 普通成员变量: 类的原型对象
  • 参数2: 成员的名字
  • 参数3: 成员属性描述符

注意: TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

// 普通访问器装饰器
function testGetAndSet(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`${propertyKey} target: ${target}`)
    descriptor.set = (val: string): void => {
       target.myName = val;
    }
    descriptor.get = (): void => {
        return target.myName;
    }
}
// 访问器装饰器工厂
function testGetAndSetFactory(parm1: number) {
    console.log(`testGetAndSetFactory parm1: ${parm1}`);
    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        descriptor.set = (val: string): void => {
           target.myName = val;
        }
        descriptor.get = (): void => {
            return target.myName;
        }
    }
}

class Person4 {
    private _name: string = "xxx";
    @testGetAndSet
    public get name() {
        return this._name;
    }
    public set name(v: string) {
        this._name = v;
    }

    private static _staticName: string = "bbb";
    @testGetAndSet
    public static get staticName() {
        return this._staticName;
    }
    public static set staticName(v: string) {
        this._staticName = v;
    }

    private _age: number = 20;
    @testGetAndSetFactory(10)
    public get age() {
        return this._age;
    }
    public set age(v: number) {
        this._age = v;
    }
}

let p4 = new Person4();
p4.name = "bbbb";
p4.age = 100;
console.log(`name: ${p4.name}`);
console.log(`age: ${p4.age}`);
console.log(p4);

// output
name target: [object Object]
testGetAndSetFactory parm1: 10
staticName target: function Person4() {...}
name: 100
age: 100
Person4 { _name: 'xxx', _age: 20 }

属性方法装饰器

参数装饰器不能用在声明文件(.d.ts). 如果访问符装饰器返回一个值,它会被用作方法的属性描述符, 属性装饰器在运行时会当做函数调用,传入2个参数

  • 参数1: 静态属性: 当前类, 普通属性: 当前类的实例
  • 参数2: 成员的名字

注意: 属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。 因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

// 普通属性装饰器
function testAttribute(target: any, propertyName: string) {
    console.log(`${propertyName} target: ${target}`);
}
// 属性装饰器工厂
function testAttributeFactory() {
    return (target: any, propertyName: string) => {
        target[propertyName] = 20;
        console.log(`${propertyName} target: ${target}`);
    }
}

class Person5 {
    @testAttribute
    static staticName: string = "1";
    @testAttribute
    name: string =  "21";
    @testAttributeFactory()
    age: number;
    @testAttributeFactory()
    age1: number = 10;
}

let p5 = new Person5();
console.log(p5.name);
console.log(`age: ${p5.age}`);
console.log(`age1: ${p5.age1}`);
console.log(p5);

//output
name target: [object Object]
age target: [object Object]
age1 target: [object Object]
staticName target: function Person5() {...}
21
age: 20
age1: 10
Person5 { name: '21', age1: 10 }

参数装饰器

参数装饰器只能用来监视一个方法的参数是否被传入, 参数装饰器的返回值会被忽略。参数装饰器不能用在声明文件(.d.ts)。参数装饰器在运行时会当做函数调用,传入3个参数

  • 参数1: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 参数2: 参数所在方法名称
  • 参数3: 参数所在参数列表中的索引
// 普通参数装饰器
function testArgument(target: any, propertyName: string, index: number) {
    console.log(`${propertyName} index: ${index} target: ${target}`)
}
// 参数装饰器工厂
function testArgumentFactory() {
    return (target: any, propertyName: string, index: number) => {
        console.log(`${propertyName} index: ${index} target: ${target}`)
    }
}
class Person6 {
    public name: string;
    static sayStatic(@testArgumentFactory() age: number) {
        console.log("sayStatic .... ")
    }

   say(@testArgumentFactory() age: number, @testArgument name: string) {
   }
}
let p6 = new Person6();
p6.say(10, "小明");
// output
say index: 1 target: [object Object]
say index: 0 target: [object Object]
sayStatic index: 0 target: function Person6() {...}
say ....

各种装饰器执行顺序

各装饰器执行的优先级:

  • 属性装饰器 | 访问器
  • 参数装饰器
  • 方法装饰器
  • 类装饰器
  • 构造方法
@testNormal1
@testNormal2
class Person8 {
    private _age1: number = 20;
    @testGetAndSet
    public get age1() {
        return this._age1;
    }
    public set age1(v: number) {
        this._age1 = v;
    }

    @testAttribute
    name: string =  "小米";

    @testAttribute
    public staticName: string =  "小米2";

    private _age: number = 20;
    @testGetAndSet
    public get age() {
        return this._age;
    }
    public set age(v: number) {
        this._age = v;
    }

    constructor() {
        console.log(`Person 构造方法`)
    }

    @testMothed
    public static say1( name: string) {
        console.log("say1");
    }
    @testMothed
    public say2(@testArgument name: string) {
        console.log("say2");
    }
    @testMothed
    public say3(name: string) {
        console.log("say3");
    }
}
let a8 = new Person8();
a8.say2(",,,");

// output
访问器装饰器 age1 target: [object Object]
属性装饰器 name target: [object Object]
属性装饰器 staticName target: [object Object]
访问器装饰器 age target: [object Object]
参数装饰器 say2 index: 0 target: [object Object]
方法装饰器 say2
方法装饰器 say3
方法装饰器 say1
普通装饰器 222 !!!
普通装饰器 111 !!!
Person 构造方法
say2 new