之前已经分别介绍了方法装饰器、属性装饰器和类装饰器,这篇文章我们来继续关注这些话题:
- 参数装饰器
- 装饰器工厂
我们将围绕以下这个例子,来探讨这些概念:
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
public saySomething(something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
参数装饰器
TypeScript对于参数装饰器的声明如下
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
如下我们为类Person
的saySomething
方法的参数添加一个参数装饰器
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
最终被编译为JavaScript的样子为:
Object.defineProperty(Person.prototype, "saySomething",
__decorate(
[__param(0, logParameter)],
Person.prototype,
"saySomething",
Object.getOwnPropertyDescriptor(Person.prototype, "saySomething")
)
);
return Person;
如果将其和之前的装饰器比较,是否会发现又使用了Object.defineProperty()
方法,那么是否意味着saySomething
将被__decorated
函数的返回值替换?
我们发现这里有个新函数__param
,TypeScript编译器生成如下:
var __param = this.__param || function (index, decorator) {
// 返回一个装饰器函数
return function (target, key) {
// 应用装饰器(忽略返回值)
decorator(target, key, index);
}
};
如上所示,调用参数装饰器,其并没有返回值,这就意味着,函数__decorate
的调用返回并没有覆盖方法saySomething
,也很好理解:参数装饰器要毛返回。
可见参数装饰器函数需要3个参数:被装饰类的原型,装饰参数所属的方法名,参数的索引。具体的实现如下:
function logParameter(target: any, key : string, index : number) {
var metadataKey = `log_${key}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else {
target[metadataKey] = [index];
}
}
其中向类的原型中增加一个新的属性metadataKey
,该属性值是一个数组,包含所装饰参数的索引,可以把它当作元数据。
参数装饰器不应当用来修改构造器、方法或属性的行为,它只应当用来产生某种元数据。一旦元数据被创建,我们便可以用其它的装饰器去读取它。
装饰器工厂
官方TypeScript装饰器建议定义一个如下的装饰器工厂:
装饰器工厂首先是一个函数,它接受任意数量的参数,同时返回如前所述的四种之一特定类型的装饰器。
虽然已经讨论四种装饰是如何实现及使用的,但还是有一些可以改进的地方,观察下面的代码片段:
@logClass
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@logMethod
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
这里装饰器的使用是没问题的,但如果我们可以不关心装饰器的类型,而在任何地方使用岂不方便,就像下面的样子:
@log
class Person {
@log
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@log
public saySomething(@log something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
这边是装饰器工厂的使用诉求,它可以识别具体情况下该使用哪种类型的装饰器,幸运的是,我们可以通过传递给装饰器的参数来区分它的类型。
function log(...args : any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if(typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}