装饰器是TypeScript提供的最强大的功能之一,它使我们能够以干净的声明性方式扩展类和方法的功能。
1、开始使用装饰器Decorators
首先我们要在tsconfig.json里面启用experimentalDecorators编译器选项:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}命令行:tsc --target ES5 --experimentalDecorators
注意事项:
- 装饰器本质上是一个函数,@expression的形式其实是一个语法糖,expression求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
- JavaScript中的Class其实也是一个语法糖。
Class Animal {
eat() {
console.log('eat food')
}
}
// 等价于
function Animal() {}
Object.defineProperty(Animal.prototype, 'eat', {
value: function() { console.log('eat food'); },
enumerable: false,
configurable: true,
writable: true
});2、装饰器的类别
【1】类装饰器
类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。
示例:
function setDefaultDesc(constructor: Function){
constructor.prototype.desc = '类装饰器属性'
}
@setDefaultDesc
class Animal {
name: string;
desc: string;
constructor() {
this.name = 'dog';
}
}
let animal= new Animal();
console.log(animal.desc) // '类装饰器属性'
// 重载函数
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
desc = "override";
}
}
@classDecorator
class Animal {
property = "property";
desc: string;
constructor(m: string) {
this.desc = m;
}
}
console.log(new Animal("world")); // Animal: {property: "property", desc: "override", newProperty: "new property" }【2】方法装饰器
方法装饰器声明在一个方法的声明之前, 它会被应用到方法属性描述符上,用来监视,修改或者替换方法定义。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- target当前对象的原型,如果Employee是对象,即Employee.prototype
- propertyName方法的名称
- descriptor方法的属性描述符Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
示例:
function logMethod(target: Object, propertyName: string, propertyDesciptor: PropertyDescriptor): PropertyDescriptor {
const method = propertyDesciptor.value;
propertyDesciptor.value = function (...args: any[]) {
// 将参数列表转换为字符串
const params = args.map(a => JSON.stringify(a)).join();
// 调用该方法并让它返回结果
const result = method.apply(this, args);
// 转换结果为字符串
const r = JSON.stringify(result);
// 在控制台中显示函数调用细节
console.log(`Call: ${propertyName}(${params}) => ${r}`);
// 返回调用的结果
return result;
}
return propertyDesciptor;
};
class Employee {
constructor(private firstName: string, private lastName: string) {}
@logMethod
greet(message: string): string {
return `${this.firstName} ${this.lastName} : ${message}`;
}
}
const emp = new Employee('三月风情', '陌上花开');
emp.greet('三月风情陌上花'); // return: '三月风情 陌上花开 : 三月风情陌上花'【3】访问器装饰器
访问器只是类声明中属性的getter和setter部分。访问器装饰器是在访问器声明之前声明的。访问器装饰器应用于访问器的属性描述符,可用于观察、修改或替换访问器的定义。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- target当前对象的原型,如果Employee是对象,即Employee.prototype
- propertyName方法的名称
- descriptor方法的属性描述符Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
示例:
function configurable(value: boolean) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}【4】属性装饰器
属性装饰器函数需要两个参数:
- target当前对象的原型
- propertyName属性的名称
示例:
function logParameter(target: Object, propertyName: string) {
// 属性的值
let _val = target[propertyName];
// 属性的get方法
const getter = () => {
console.log(`Get: ${propertyName} => ${_val}`);
return _val;
};
// 属性的set方法
const setter = newVal => {
console.log(`Set: ${propertyName} => ${newVal}`);
_val = newVal;
};
// 删除属性
if (delete target[propertyName]) {
// 使用getter和setter创建新属性
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
class Employee {
@logParameter
name: string;
}
const emp = new Employee();
emp.name = '陌上花开'; // Set: name => 陌上花开
console.log(emp.name);// Get: name => 陌上花开【5】参数装饰器
参数装饰器函数需要三个参数:
- target当前对象的原型
- propertyName参数的名称
- index参数在参数数组中的位置
示例:
function logParameter(target: Object, propertyName: string, index: number) {
// 为相应的方法生成元数据,保持被修饰参数的位置
const metadataKey = `log_${propertyName}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
} else {
target[metadataKey] = [index];
}
}
class Employee {
greet(@logParameter message: string): string {
return `hello ${message}`;
}
}
const emp = new Employee();
emp.greet('hello');【6】装饰器工厂
假设我们需要几个装饰器分别把一个类中的部分属性、类本身、方法、参数的名称打印出来,这时候我们该怎么做。
import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';
// 假设我们已经有了上面这些装饰器,下面我们就该这样做
function log(...args) {
switch (args.length) {
case 3:
// 可以是方法装饰器还是参数装饰器
if (typeof args[2] === "number") {
// 如果第三个参数是number那么它的索引就是它的参数装饰器
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
case 2:
// 属性装饰器
return logProperty.apply(this, args);
case 1:
// 类装饰器
return logClass.apply(this, args);
default:
// length长度在1,2,3外的情况
throw new Error('Not a valid decorator');
}
}
@log
class Employee {
@log
private name: string;
constructor(name: string) {
this.name = name;
}
@log
greet(@log message: string): string {
return `${this.name} says: ${message}`;
}
}【7】元数据Reflection API
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}
}
Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'- Relfect Metadata,可以通过装饰器来给类添加一些自定义的信息
- 然后通过反射将这些信息提取出来
- 也可以通过反射来添加这些信息
function logParameter(target: Object, propertyName: string, index: number) {
// 从目标对象获取元数据
const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
indices.push(index);
// 将元数据定义为目标对象
Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}
// 属性装饰器使用反射api来获取属性的运行时类型
export function logProperty(target: Object, propertyName: string): void {
// 从对象中获取属性的设计类型
var t = Reflect.getMetadata("design:type", target, propertyName);
console.log(`${propertyName} type: ${t.name}`); // name type: String
}
class Employee {
@logProperty
private name: string;
constructor(name: string) {
this.name = name;
}
greet(@logParameter message: string): string {
return `${this.name} says: ${message}`;
}
}在上面的例子中,我们使用了反射元数据设计键[design:type]。目前只有三种:
- 类型元数据使用元数据键design:type
- 参数类型元数据使用元数据键design:paramtypes
- 返回类型的元数据使用元数据键design:returntype