装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
JavaScript的装饰器提案历经一波三折,目前仍处于Stage 2阶段,而且在语法和实现上经历了较大的改版,距离正式成为ECMA语言标准尚需时日。在TypeScript愈发流行的今天,它已推出了这个实验性功能,一些框架如angular、nestjs都已经大量使用了装饰器。
介绍
装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器。
参考lib.es5.d.ts
中ECMAScript APIs
部分对于装饰器的定义:
1 2 3 4 5 6 7 8 9
|
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
|
类装饰器
接口定义:
1
| declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
|
类装饰器表达式,由定义知道,传入1个参数:
target
—— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
先看一个最简单的装饰器,普通装饰器,只有一个参数target,当把这个@helloWord装饰器作用在HelloWordClass类上,这个target参数传递的就是HelloWordClass类。
1 2 3 4 5 6 7 8 9 10 11
| function helloWord(target: any) { console.log('hello Word!'); }
@helloWord class HelloWordClass { constructor() { console.log('我是构造函数') } name: string = 'mulianju'; }
|
执行结果:
类装饰器3种类型
- 普通装饰器(无法传参)
- 装饰器工厂(可传参)
- 重载构造函数
普通装饰器(无法传参)
1 2 3 4 5 6 7 8 9 10 11
| function helloWord(target: any) { console.log('hello Word!'); }
@helloWord class HelloWordClass { constructor() { console.log('我是构造函数') } name: string = 'mulianju'; }
|
装饰器工厂(可传参)
增加了一个静态变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function helloWord(isTest: boolean) { return function(target: any) { target.isTestable = isTest; } }
@helloWord(false) class HelloWordClass { constructor() { console.log('我是构造函数') } name: string = 'mulianju'; } let p = new HelloWordClass(); console.log(HelloWordClass.isTestable);
|
重载构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function helloWord(target: any) { return class extends target { sayHello(){ console.log("Hello") } } }
@helloWord class HelloWordClass { constructor() { console.log('我是构造函数') } name: string = 'mulianju'; }
|
属性装饰器
接口定义:
1
| declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
|
属性装饰器表达式会在运行时当作函数被调用,由定义知道,传入2个参数:
target
—— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey
—— 属性的名称。
- 没有返回值。
按照上面的接口形式,定义了一个defaultValue()装饰器方法,就算是用private也是能生效的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function defaultValue(value: string) { return function (target: any, propertyName: string) { target[propertyName] = value; } }
class HelloWordClass { constructor() { console.log('我是构造函数') } @defaultValue('mulianju') private name: string | undefined; } let p = new HelloWordClass(); console.log(p.name);
|
输出结果:
1 2
| 我是构造函数 mulianju // 这里打印出设置的默认值
|
通过属性描述来修改属性值
对于属性的装饰器,是没有返回descriptor的,并且装饰器函数的返回值也会被忽略掉。还可以通过自己获取descriptor,并进行修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function defaultValue(value: string) { return function (target: any, propertyName: string) { let descriptor = Object.getOwnPropertyDescriptor(target, propertyName);
Object.defineProperty(target, propertyName, { ...descriptor, value }) } }
class HelloWordClass { constructor() { console.log('我是构造函数') } @defaultValue('mulianju') private name: string | undefined; } let p = new HelloWordClass(); console.log(p.name);
|
输出结果:
1 2
| 我是构造函数 mulianju // 这里打印出设置的默认值
|
这种方式通过Object.getOwnPropertyDescriptor和Object.defineProperty方法对属性进行定义。这种方式适用面更广,可以针对属性描述进行修改。
方法装饰器
接口定义:
1
| declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
|
方法装饰器接受三个参数:
target
—— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey
—— 属性的名称。
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
| function logFunc(params: string) { return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
console.log(params); const method = descriptor.value; descriptor.value = function (...args: any[]) { let start = new Date().valueOf(); args = args.map(arg => String(arg)); console.log('参数args = ' + args); try { return method.apply(this, args) } finally { let end = new Date().valueOf(); console.log(`start: ${start} end: ${end} consume: ${end - start}`) } }; return descriptor; } }
class HelloWordClass { constructor() { console.log('我是构造函数') } private nameVar: string | undefined;
@logFunc('log装饰器') sayHello(name: string) { console.log(name + ' sayHello'); } } let pHello = new HelloWordClass(); pHello.sayHello('mulianju');
|
输出结果:
1 2 3 4 5
| log装饰器 我是构造函数 参数args = mulianju mulianju sayHello start: 1574331292433 end: 1574331292434 consume: 1
|
方法参数装饰器
接口定义:
1
| declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
|
方法参数装饰器会接收三个参数:
target
—— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey
—— 属性的名称。
parameterIndex
—— 参数数组中的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function logParameter(target: any, propertyName: string, index: number) { const metadataKey = `log_${propertyName}_parameters`; if (Array.isArray(target[metadataKey])) { target[metadataKey].push(index); } else { target[metadataKey] = [index]; } }
class HelloWordClass { constructor() { console.log('我是构造函数') } private nameVar: string | undefined;
sayHello(@logParameter name: string) { console.log(name + ' sayHello'); } } let pHello = new HelloWordClass(); pHello.sayHello('mulianju');
|
访问器装饰器
访问器就是添加有get、set前缀的函数,用于控制属性的赋值及取值操作。
访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
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
| function enumerable(value: boolean) { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.enumerable = value; }; }
class HelloWordClass { constructor() { console.log('我是构造函数') }
private _name: string = 'mulianju'; private _age: number = 10;
@enumerable(true) get name() { return this._name; }
set name(name: string) { this._name = name; }
@enumerable(false) get age() { return this._age; }
set age(age: number) { this._age = name; } } let pHello = new HelloWordClass(); for (let prop in pHello) { console.log(`property = ${prop}`); }
|
enumerable
属性描述符:
当且仅当该属性的enumerable
为true
时,该属性才能够出现在对象的枚举属性中。通过Object.defineProperty()
创建属性,enumerable
默认为false
。
如果一个属性的enumerable为false,通过对象还是能访问的到这个属性,但下面三个操作不会取到该属性。
for…in
循环
Object.keys
方法
JSON.stringify
方法
我们定义了两个访问器 name 和 age,并通过装饰器设置是否将其列入清单,据此决定对象的行为。name 将列入清单,而 age 不会。
所以控制台输出结果:
1 2 3 4 5
| 我是构造函数 property = _name property = _age property = name // 少了一个属性age
|
装饰器执行顺序
类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
注意:TypeScript 不允许同时装饰单一成员的 get 和 set 访问器。这是因为装饰器可以应用于属性描述符,属性描述符结合了 get 和 set 访问器,而不是分别应用于每项声明。
装饰器模式的优缺点
优点
- 动态扩展类功能,比类继承灵活,且对客户端透明;
- 继承关系的一种替代方案。相比与类继承的父子关系,装饰模式 更像是一种-组合关系(is-a);
- 可以对同一个被装饰对象进行多次装饰,创建出不同行为的复合功能。
缺点
- 多层装饰比较复杂(灵活的同时会带来复杂性的增加);
- 装饰嵌套过多,会产生过多小对象(每个装饰层都创建一个相应的对象);
- 装饰嵌套过多,易于出错,且调试排查比较麻烦(需要一层一层对装饰器进行排查,以确定是哪一个装饰层出错)。
参考资料:
本文永久链接: https://www.mulianju.com/decorator/