Please enable Javascript to view the contents

装饰器是什么

 ·  ☕ 6 分钟

在NestJs中大量使用了装饰器,所以学习NestJs前首先需要了解装饰器的基本用法。

装饰器可以解决的问题:

  1. 一个类、一个方法应该专注于处理一块单一功能的逻辑(即单一功能原则),但在实际的业务场景中,我们经常会在业务代码中插入额外的逻辑代码。
    比如统计行为、性能,记录日志等。使用装饰器可以让代码保持单一原则,把这些额外的代码抽离出来让代码更简洁,另一方面也对这些额外的逻辑代码做了封装提高了复用性。

  2. 又或是需要解决强依赖的过度耦合问题,比如过于依赖一个a类来实现另一个新的b类,当a类需要修改的时候b类也需要修改。
    使用装饰器可以做到依赖注入从到实现依赖反转。


模拟装饰器

有一个User类,其中它有自己的login方法。现在我们需要在它的登录方法中添加登录时间的日志记录逻辑。
通常我们会直接在login方法中添加日志逻辑,但这回我们需要不直接修改login方法。

js中有一个方法叫Object.defineProperty,熟悉vue2的同学会知道这个API,它可以用来定义对象的属性。
它也可以用来定义类的方法并返回这个类本身。

Object.defineProperty(target, key, descriptor)
如果key对应的是普通方法的名字,target就是类的原型
如果key对应的是静态方法的名字,target就是类
在descriptor中可以获取到这个方法的描述符,比如value、writable、enumerable、configurable等
如果key对应的是一个方法名,value就是这个方法的引用

模拟装饰器的实现步骤:

  1. 获取类原本的方法内容(即descriptor),可以通过Object.getOwnPropertyDescriptor(target, key);。
  2. 修改这个方法,比如在原有的方法上加入登录日志逻辑。
  3. 用新的方法覆盖原有的方法,可以通过Object.defineProperty(target, key, descriptor); 。

这个就是类装饰器想要达到的目的。


装饰器不同类型

1. 类装饰器

从模拟类装饰器中我们得出需要在原有类的基础上生成一个新的类,可以用继承或者修改原型的方法达到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const happyHuman = <T extends new (...args: any[]) => any>(
    constructor: T,
) => {
    return class extends constructor {
        name = 'happy human'; // 装饰器的属性会覆盖类的属性

        getName() { // 会覆盖类已有方法,或添加新的类方法
            return this.name;
        }
    };
};

@happyHuman
class User {
    [key: string]: any;

    name: string;

    constructor() {
        this.name = 'human';
    }
}

console.log(new User().getName()); // happy human

当类装饰器需要传参数时,可以把参数套在装饰器函数的外面(高阶函数),这样就可以在装饰器函数内部使用这个参数了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const setNameDecorator = (firstName: string, lastName: string) => {
    const fullName = `${firstName}.${lastName}`;
    return <T extends new (...args: any[]) => any>(target: T) => {
        return class extends target {
            getName() {
                return `My name is ${fullName}.`;
            }
        };
    };
};

@setNameDecorator('Llane', 'zhang')
class User {
    [key: string]: any;

    constructor() {
        this.name = 'human';
    }

    getName() {
        return this.name;
    }
}

console.log(new User().getName()); // My name is Llane.zhang.

除了使用类继承方法来生成一个新的类,我们还可以修改类的原型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
type TUserProfile = {
    firstName: string;
    lastName: string;
    [key: string]: any;
};

const ProfileDecorator = (profile: TUserProfile) => {
    const { firstName, lastName } = profile;
    const fullName = `${firstName}.${lastName}`;
    return <T extends new (...args: any[]) => any>(target: T) => {
        const Original = target;
        // 在原型上添加一个新属性
        Original.prototype.fullName = fullName;
        // 修改原型上的方法
        Original.prototype.getName = function () {
            return `My name is ${fullName}.`;
        };
        // override构造函数
        function constructor(...args: any[]) {
            return new Original(...args);
        }
        // 赋值原型链
        constructor.prototype = Original.prototype;
        // 添加一个静态方法
        constructor.myInfo = `myInfo ${JSON.stringify(profile)}`;
        return constructor as unknown as typeof Original;
    };
};

@ProfileDecorator({ firstName: 'Llane', lastName: 'zhang', age: 25 })
class User {
    [key: string]: any;

    constructor() {
        this.name = 'human';
    }

    getName() {
        return this.name;
    }
}

interface StaticUser {
    new (): TUserProfile;
    myInfo: string;
}

console.log(new User().getName()); // My name is Llane.zhang.
console.log((User as unknown as StaticUser).myInfo); // myInfo {"firstName":"Llane","lastName":"zhang","age":25}


2. 属性装饰器

一般不单独使用,主要用于配合类或方法装饰器进行组合装饰

属性装饰器主要有两个参数

  • target 普通属性时为当前对象原型 即:x.prototype,静态属性时为当前对象的类
  • propertyKey 属性名称
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 这里储存属性装饰器中存的参数
const userRoles: string[] = [];

const RoleDecorator =
    (roles: string[]) => 
    (target: any, key: string) => {
        roles.forEach((role) => userRoles.push(role));
    };

const SetRoleDecorator = <T extends new (...args: any[]) => { [key: string]: any },>(
    constructor: T,
) => {
    const roles = [
        { name: 'super-admin', desc: '超级管理员' },
        { name: 'admin', desc: '管理员' },
        { name: 'user', desc: '普通用户' },
    ];
    return class extends constructor {
        constructor(...args: any) {
            super(...args);
            this.roles = roles.filter((role) => userRoles.includes(role.name));
        }
    };
};

@SetRoleDecorator
class UserEntity {
    @RoleDecorator(['admin', 'user'])
    roles: string[] = [];
}

const user = new UserEntity();
console.log(user.roles); // [ { name: 'admin', desc: '管理员' }, { name: 'user', desc: '普通用户' } ]


3. 方法装饰器

方法装饰器接受三个参数(比属性装饰器多一个方法的属性描述符 descriptor)

  • target 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey 方法名
  • descriptor 方法的属性描述符
    其中属性描述符有以下几个子属性
  • configurable?: boolean; 是否能修改或删除这个属性
  • enumerable?: boolean; 该属性是否可枚举可被遍历出来
  • value?: any; 用于定于新的方法来替换旧方法(请使用function 以免内部不能使用this来绑定)
  • writable?: boolean; 是否可写
  • get?: any; get访问器
  • set(v: any)?: void; set访问器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const LoggerDecorator = () => {
    return function logMethods(
        target: any,
        propertyName: string,
        propertyDescriptor: PropertyDescriptor,
    ): PropertyDescriptor {
        // 保留原有的登录逻辑
        const methods = propertyDescriptor.value;
        // 重写方法
        propertyDescriptor.value = async function (...args: any[]) {
            try {
                await methods.apply(this, args);
            } finally {
                const now = new Date().valueOf();
                console.log(`logged in ${now}`);
            }
        };
        return propertyDescriptor;
    };
};

class UserService {
    @LoggerDecorator()
    async login() {
        await new Promise((resolve) => {
            setTimeout(resolve, 2000);
        });
        console.log('login success');
    }
}

const user = new UserService();
user.login(); // login success // logged in 1679550405528

4. 参数装饰器

每个类方法的入参也可以有自己的装饰器,一般也配合类或方法装饰器使用

参数装饰器的参数有:
target 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
key 参数名
index 参数在函数参数列表中的索引

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 创造一个专门用于格式化入参的装饰器

// 储存格式化入参方法
const argsParse: ((...args: any[]) => any)[] = [];

const parse =
    (parseFn: (...args: any[]) => number) =>
    (target: any, key: string, index: number) => {
        argsParse[index] = parseFn;
    };

const parseDecorator = (
    target: any,
    propertyName: string,
    descriptor: PropertyDescriptor,
): PropertyDescriptor => {
    const method = descriptor.value;
    return {
        ...descriptor,
        value(...args: any[]) {
            const newArgs = args.map((value, index) => {
                return argsParse[index] ? argsParse[index](value) : value;
            });
            return method.apply(this, newArgs);
        },
    };
};

interface IUser {
    id: number;
    name: string;
}

class UserService {
    private users: IUser[] = [
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Ben' },
    ];

    getUsers() {
        return this.users;
    }

    @parseDecorator
    delete(@parse((arg: any) => Number(arg)) id: any) {
        this.users = this.users.filter((user) => user.id !== id);
    }
}

const userService = new UserService();
console.log('before delete 1', userService.getUsers());
userService.delete(1);
console.log('after delete 1', userService.getUsers());

console.log('before delete 2', userService.getUsers());
userService.delete('2'); // 这里将入参string格式化为number了,所以依旧可以运行成功
console.log('after delete 2', userService.getUsers());

// before delete 1 [ { id: 1, name: 'Alice' }, { id: 2, name: 'Ben' } ]
// after delete 1 [ { id: 2, name: 'Ben' } ]
// before delete 2 [ { id: 2, name: 'Ben' } ]
// after delete 2 []


5. 访问器装饰器

参数和方法装饰器一样(其实就是属性的get 和 set方法)
通过访问器装饰器可以重写get和set方法和descriptor的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export const HiddenDecorator = () => {
    return (
        target: any,
        propertyName: string,
        descriptor: PropertyDescriptor,
    ) => {
        descriptor.enumerable = false;
    };
};

const PrefixDecorator = (prefix: string) => {
    return (
        target: any,
        propertyName: string,
        descriptor: PropertyDescriptor,
    ) => {
        return {
            ...descriptor,
            set(value: string) {
                descriptor.set.apply(this, [`${prefix}_${value}`]);
            },
        };
    };
};

class UserEntity {
    private _nickName: string;

    private fullName: string;

    @HiddenDecorator()
    @PrefixDecorator('Llane')
    get nickName() {
        return this._nickName;
    }

    set nickName(value: string) {
        this._nickName = value;
        this.fullName = `${value}_fullName`;
    }

    getFullName() {
        return this.fullName;
    }
}

const user = new UserEntity();
console.log(user); // UserEntity { _nickName: undefined, fullName: undefined }
console.log(user.nickName);
user.nickName = 'goodman';
console.log(user.nickName); // Llane_goodman


装饰器的调用顺序

参数 -> 方法 -> get/set/属性 -> 类
当对一个被装饰者调用多个装饰器的时候

1
2
3
4
5
@A
@B
getName() => {
  return this.name;
}

上述代码相当与A(B(getName))
它们的调用步骤类似剥洋葱法,即:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用。
分享

Llane00
作者
Llane00
Web Developer