TypeScript装饰器与设计模式
什么是装饰器
在内部,装饰器只是一个函数。在 js 执行时,将目标(类,函数,属性等)传入装饰器,并执行。
首先装饰器是 js/ts 的一个实验属性,我们需要在 tsconfig.json 中找到"experimentalDecorators": true,
并打开它。
类装饰器
首先装饰器的函数名我们通常情况下使用大驼峰命名法,然后函数的参数类型取决于这个装饰器作用于哪里。比如这是个类装饰器,参数的类型就为Function
,它是作用于构造函数的。这里的参数也是指的传入的类的构造函数。
在使用类装饰器的时候将其写在类的上面(注意没有括号)
function Component(constructor: Function): void {
console.log("Component decorator called");
constructor.prototype.uniqueId = Date.now();
constructor.prototype.inserInDOM = () => {
console.log("Inserting the component in the DOM");
};
}
@Component
class ProfileComponent {}
//Component decorator called
装饰器工厂
现在我们想要创建参数化的装饰器
// 装饰器工厂/Decorator factory
function Component(value: number) {
return (constructor: Function) => {
console.log("Component decorator called");
constructor.prototype.options = value;
constructor.prototype.uniqueId = Date.now();
constructor.prototype.inserInDOM = () => {
console.log("Inserting the component in the DOM");
};
};
}
@Component(1) //Component decorator called number 1
class ProfileComponent {}
这看起来就像一个工厂,用于创建装饰器。这样的函数叫做装饰器工厂。
我们现在让参数为对象:
type ComponentOptions = {
selector: string;
};
// 装饰器工厂/Decorator factory
function Component(options: ComponentOptions) {
return (constructor: Function) => {
console.log("Component decorator called");
constructor.prototype.options = options;
constructor.prototype.uniqueId = Date.now();
constructor.prototype.inserInDOM = () => {
console.log(
"Inserting the component in the DOM:" +
constructor.prototype.options.selector
);
};
};
}
@Component({ selector: "#my-profile" })
class ProfileComponent {
inserInDOM() {} //需要声明
}
let profileComponent = new ProfileComponent();
profileComponent.inserInDOM(); //Inserting the component in the DOM:#my-profile
使用多个装饰器
我们可以同时使用多个装饰器
type ComponentOptions = {
selector: string;
};
// 装饰器工厂/Decorator factory
function Component(options: ComponentOptions) {
return (constructor: Function) => {
console.log("Component decorator called");
constructor.prototype.options = options;
constructor.prototype.uniqueId = Date.now();
constructor.prototype.inserInDOM = () => {
console.log(
"Inserting the component in the DOM:" +
constructor.prototype.options.selector
);
};
};
}
function Pipe(constructor: Function) {
console.log("Pipe decorator called");
constructor.prototype.pipe = true;
}
@Component({ selector: "#my-profile" })
@Pipe
class ProfileComponent {
inserInDOM() {}
}
// Pipe decorator called
// Component decorator called
需要注意的是,我们的装饰器是按照相反的顺序应用的。
这背后的想法来自数学:在数学中如果我们有f(g(x))
这样的表达式,然后我们会先求得 g(x)的值然后把它传给 f(x)。
方法装饰器
方法装饰器有三个参数:
参数 | 说明 |
---|---|
参数一 | 普通方法是构造函数的原型对象 Prototype,静态方法是构造函数 |
参数二 | 方法名称 |
参数三 | 属性特征 |
我们这里不会用到 target 和 methodName,但由于我们又在 tsconfig.josn 中进行了配置,我们可以关闭这个配置,也可以使用带下划线_
的前缀来忽略这个报错。
function Log(
_target: any,
_methodName: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value as Function;
descriptor.value = function () {
console.log("Before");
original.call(this, "传入数据");
console.log("After");
};
}
class Person {
@Log
say(message: string) {
console.log("Person says " + message);
}
}
let person = new Person();
person.say("dom");
// Before
// Person says 传入数据
// After
我们发现 target 的类型使用的 any,虽然我们建议尽量不用 any,但也不是完全不用,我们在这里并不知道 target 的类型。method 的类型为 string,descriptor 属性特征的类型为 PropertyDescriptor。
我们在覆盖descriptor.value
前将原方法保留并在新方法中调用。
我们发现我们在实例对象传的参数会被忽略。因为在我们的新方法中没有参数,而是直接调用的保存好的原函数original
。
如果我们像不被覆盖,我们可以这样写:
function Log(
_target: any,
_methodName: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value as Function;
descriptor.value = function (message: string) {
console.log("Before");
original.call(this, message);
console.log("After");
};
}
class Person {
@Log
say(message: string) {
console.log("Person says " + message);
}
}
let person = new Person();
person.say("dom");
// Before
// Person says dom
// After
为了让这个装饰器能够在多个方法中使用,我们可以这样做:
function Log(
_target: any,
_methodName: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value as Function;
descriptor.value = function (...args: any) {
console.log("Before");
original.call(this, ...args);
console.log("After");
};
}
值得注意的是:我们在写新方法时应该用函数表达式声明,而不是箭头函数声明。因为箭头函数没有自己的 this,他们的 this 指向当前实例对象
在访问器中使用装饰器
在 getter 和 setter 访问器中我们应该如何使用装饰器呢?
访问器与方法类似,所以我们和使用方法装饰器的时候一样,唯一不同的是,访问器不能使用descriptor
的value
属性,在使用 get 访问器的时候我们要使用get
属性。而不是value
function Capitalize(
_target: any,
_methodName: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.get;
descriptor.get = function () {
const result = original?.call(this);
return typeof result === "string" ? result.toUpperCase() : result;
};
}
class Person {
constructor(public firstName: string, public lastName: string) {}
@Capitalize
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
let person = new Person("Kevin", "Qian");
console.log(person.fullName); //KEVIN QIAN
属性装饰器
这里我们使用了装饰器工厂
属性装饰器的参数为:
参数 | 说明 |
---|---|
参数一 | 普通方法是构造函数的原型对象 Prototype,静态方法是构造函数 |
参数二 | 属性名称 |
function MinLength(length: number) {
return (target: any, propertyName: string) => {
let value: string; // 注意要先声明
const descriptor: PropertyDescriptor = {
get() {
return value;
},
set(newValue: string) {
if (newValue.length < length)
throw new Error(`${propertyName} should be at least ${length}`);
value = newValue;
},
};
Object.defineProperty(target, propertyName, descriptor); // 我们通过这个方法来改变我们的原属性。
};
}
class User {
@MinLength(4)
password: string;
constructor(password: string) {
this.password = password;
}
}
let user = new User("5678");
console.log(user.password); //5678
// user.password = "22"; //Error: password should be at least 4
// let errUser = new User("111"); //Error: password should be at least 4
我们可以看到,传入构造函数的值如果长度小于 4,则会报错。并且在重新赋值属性的时候,装饰器会被重新调用。进行一个验证,当长度小于 4 的时候报错。
参数装饰器
我们并不常使用参数装饰器,但如果你正在设计一个框架供其他人使用,你可能会用到参数装饰器。
我们通常情况将其用于存储这些参数的一些元数据
type WatchedParameter = {
methodName: string;
parameterIndex: number;
};
const watchedParameters: WatchedParameter[] = [];
function Watch(_target: any, methodName: string, parameterIndex: number) {
watchedParameters.push({
methodName,
parameterIndex,
});
}
class Vehicle {
move(@Watch _speed: number) {}
}
console.log(watchedParameters); //[ { methodName: 'move', parameterIndex: 0 } ]