分享一下学习装饰器(
Decorators
)的所得
概念介绍
装饰器是TypeScript
提供的最强大的功能之一,它使我们能够以干净的声明性方式扩展类和方法的功能。装饰器目前是JavaScript
的第2阶段提议,但在TypeScript
生态系统中已受到欢迎,主要的开放源代码项目(例如Angular
)正在使用装饰器。
本人工作中是使用Angular8进行项目一个项目的开发的,接触装饰器这一方面也就比较多,所以就趁着周末整理了一篇关于装饰器Decorators
的文章,也希望能帮助到学习这方面的同学们。废话不多说,下面咱们进入正题。
开始使用装饰器Decorators
首先我们要在tsconfig.json
里面启用experimentalDecorators编译器选项:
命令行:tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
我们先明确两个概念:
- 目前装饰器本质上是一个函数,
@expression
的形式其实是一个语法糖,expression
求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。 JavaScript
中的Class
其实也是一个语法糖。
例如我们在Javascript
中声明一个Class
:
Class Animal {
eat() {
console.log('eat food')
}
}
上面这个Animal类实际等同于下面这样:
function Animal() {}
Object.defineProperty(Animal.prototype, 'eat', {
value: function() { console.log('eat food'); },
enumerable: false,
configurable: true,
writable: true
});
类装饰器
类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。
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" }
这部分代码的含义是:被classDecorator
装饰的类里面如果不存在newProperty
或desc
属性,会增加相应的属性和对应的value
,如果存在该属性就会重写该属性的value
。
方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- target 当前对象的原型 (如果Employee是对象,即
Employee.prototype
) - propertyKey 方法的名称
- descriptor 方法的属性描述符
Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
function logMethod(
target: Object,
propertyName: string,
propertyDesciptor: PropertyDescriptor): PropertyDescriptor {
// target === Employee.prototype
// propertyName === "greet"
// propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")
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: '三月风情 陌上花开 : 三月风情陌上花'
访问器装饰器
访问器只是类声明中属性的getter和setter部分。 访问器装饰器是在访问器声明之前声明的。访问器装饰器应用于访问器的属性描述符,可用于观察、修改或替换访问器的定义。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- target 当前对象的原型 (如果Employee是对象,即
Employee.prototype
) - propertyKey 方法的名称
- descriptor 方法的属性描述符
Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
下面是使用了访问器装饰器(@configurable
)的例子,应用于Point
类的成员上:
function configurable(value: boolean) {
return function (target: any, propertyKey: 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; }
}
属性装饰器
属性装饰器函数需要两个参数:
- target 当前对象的原型
- propertyKey 属性的名称
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 => 陌上花开
// 陌上花开
参数装饰器
参数装饰函数需要三个参数:
- target 当前对象的原型
- propertyKey 方法的名称
- 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');
在上面的代码示例中:target
为Employee的实例emp
,propertyName
的值为greet
,index
的值为0
。
装饰器工厂
我们先假设这样一个场景,比如我们需要几个装饰器,分别把一个类中的部分属性、类本身、方法、参数的名称打印出来,这时候我们该怎么做。
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}`;
}
}
装饰器工厂就是一个简单的函数,它返回一种类型的装饰器。
元数据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,可以通过装饰器来给类添加一些自定义的信息
- 然后通过反射将这些信息提取出来
- 也可以通过反射来添加这些信息
反射, ES6+ 加入的 Relfect 就是用于反射操作的,它允许运行中的 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性和方法,反射这个概念其实在 Java/c# 等众多语言中已经广泛运用了
再来一个小例子来看下:
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
总结
这篇文章主要介绍了类装饰器,方法装饰器,访问器装饰器,属性装饰器,参数装饰器,装饰器工厂,和元数据Reflection。也是我学习过程中的一些总结。每周都会持续更新不同的技术,喜欢的同学可以点赞加关注,大家一起进步。如果有想学习某方面技术的同学也欢迎评论区留言,我会努力写出大家感兴趣的内容。