装饰器是一项实验性特性,在未来的版本中可能会发生改变,可以通过配置开启装饰器功能
// tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
类装饰器
@f
@g
class Person {
// ...
}
只有一个参数,即构造函数,当书写多个装饰器时,执行顺序如下
1、从上到下依次求值
2、求值的结果会被当成函数,从下往上调用
感觉有点像 koa 的洋葱模型,这样会出现一个很有趣的现象,如下所示
@f()
@g()
class Person {}
function f(): ClassDecorator {
console.log("f outer");
return function (target) {
console.log("f inner", target);
};
}
function g(): ClassDecorator {
console.log("g outer");
return function (target) {
console.log("g inner", target);
};
}
const p = new Person()
/**
* f outer
* g outer
* g inner
* f inner
*/
不同种类的装饰器也存在调用顺序,顺序如下
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
接下来是一个修改类的构造函数以及其方法的🌰
interface UserType {
login(username: string, b: number, c: boolean): void;
}
@classDecorator()
class User implements UserType {
login(a: string, b: number, c: boolean) {
console.log("user execute");
}
}
type ConstructorType<T> = new (...args: any[]) => T
type CustomClassType<T> = (target: ConstructorType<T>) => ConstructorType<T>
function classDecorator(): CustomClassType<UserType> {
return function (target) {
return class extends target {
newProperty = "new property";
hello = "override";
login() {
console.log('chaned')
}
};
};
}
const user = new User();
console.log(user);
user.login('name', 1, true)
方法装饰器
传入的参数如下
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 成员的名字
- 成员的属性描述符
interface UserType {
login(username: string, b: number, c: boolean): void;
}
class User implements UserType {
@loginDecorator() login(a: string, b: number, c: boolean) {
console.log("user execute");
}
@staticDecorator() static staticMethod() {
console.log("staticMethod");
}
}
function loginDecorator(): MethodDecorator {
return function (target, key, descriptor) {
console.log("loginDecorator", target, key, descriptor);
};
}
function staticDecorator(): MethodDecorator {
return function (target, key, descriptor) {
console.log("staticDecorator", target, key, descriptor);
};
}
const user = new User();
user.login("name", 1, true);
User.staticMethod();
loginDecorator 和 arrowDecorator 中的 target 都是实例的原型对象,包含 login 方法,目前不会其他的标注方法,先复用类的接口签名
写到这里产生一个疑惑,<T extends UserType> 和直接标注 UserType 有区别吗,答案是有的,根据场景选择使用
T extends UserType
T 可以是 UserType 的子类型,或者是包含 UserType 所有属性的其他类型,编译器可以根据传入的类型来推断 T 的具体类型,从而提供更精确的类型检查
直接标注 UserType
该对象必须完全符合 UserType 的定义,不允许为其子类型,而且编译器不会进行类型推断,因为其类型已经固定
访问器装饰器
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 成员的名字
- 成员的属性描述符
如果同时定义属性的 get / set,需要统一写在 get 之前,举个🌰
class Person {
_count: number;
constructor(count: number) {
this._count = count;
}
@getDecorator()
@setDecorator()
get count(): number {
return this._count;
}
set count(val: number) {
this._count = val;
}
}
type AccessorDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void
function getDecorator(): AccessorDecorator {
return function (
target,
key,
descriptor
) {
const originalGet = descriptor.get;
descriptor.get = function () {
return originalGet ? originalGet.call(this) : descriptor.value;
};
};
}
function setDecorator(): AccessorDecorator {
return function (
target,
key,
descriptor
) {
const originSet = descriptor.set;
descriptor.set = function (val: number) {
if (originSet) {
originSet.call(this, val * 2);
} else {
(this as InstanceType<typeof Person>)._count = val * 2;
}
};
};
}
const p: InstanceType<typeof Person> = new Person(10);
console.log(p.count);
p.count = 20;
console.log(p.count);
属性装饰器
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 成员的名字
下面使用 reflect-metadata 配合装饰器,实现元数据的挂载,可以看到,在声明装饰器时,传入了一个参数,然后使用 Reflect.metadata 挂载到原型中,当外界读取 formatText 时,调用 getMetadata 函数,从实例中读取对应的 value,虽然元数据定义到了原型中,但是通过原型链的查找,是可以正常工作的
import 'reflect-metadata'
const key = Symbol('text')
class Animal {
@setMetadata('hello $1')
text: string;
constructor(text: string) {
this.text = text;
}
get formatText() {
const string = getMetadata(this, key);
return string.replace('$1', this.text)
}
}
function setMetadata(value: string) {
return function(target: object, key: string) {
return Reflect.metadata(key, value, target)
}
}
function getMetadata(target: object, propertyKey: string | symbol) {
return Reflect.getMetadata(key, target, propertyKey)
}
元数据挂载的原理,在 target 中,设置一个 [[metadata]] 的属性,该属性的值是一个 WeakMap,其中的 key 就是设置元数据的第一个参数,value 为所设置的 value,所以 key 也可以为除了字符串,Symbol 之外的其他类型,不过为了防止命名污染,还是会使用 Symbol 来当作 key,需要注意,interface、type 等类型声明是无法参与元数据的记录的,因为元数据在运行时也会存在
还有一种简写方式,如下,在原型中添加了一个字符串,其代表了 getRecord 的返回类型,然后读取
// 原生写法
const ReturnTypeMetaKey = Symbol("design:returntype");
class Point {
private x: number;
private y: number;
constructor(x, y) {
this.x = x;
this.y = y;
}
@ReturnType("[number, number]")
getCoord() {
return [this.x, this.y];
}
}
function ReturnType(type: string) {
return Reflect.metadata(ReturnTypeMetaKey, type)
}
const p = new Point(50, 100)
Reflect.getMetadata(ReturnTypeMetaKey, p, 'getRecord')
// "[number, number]"
// 简写,结果不变
const ReturnTypeMetaKey = Symbol("design:returntype");
class Point {
private x: number;
private y: number;
constructor(x, y) {
this.x = x;
this.y = y;
}
@Reflect.metadata(ReturnTypeMetaKey, "[number, number]")
getCoord() {
return [this.x, this.y];
}
}
const p = new Point(50, 100)
Reflect.getMetadata(ReturnTypeMetaKey, p, 'getRecord')
// "[number, number]"
以下是原始的装饰器写法,可以看到,存在两种写法,这两种写法的相同点是,对装饰器求值后,会返回一个函数,并且传入对应的参数,不同点在声明时会有所区分,那么为什么上面的元数据可以简写呢,说明 Reflect.metadata 会返回一个装饰器函数,接收对应的参数,并且在 ts 编译时执行,在运行时就可以读取了
@f
class T {
@methodDecorator()
method() {
}
}
function f(target: object) {
// target 为类的构造函数
}
function methodDecorator () {
return function(target: object, key: string, descriptor: PropertyDescriptor) {
// target 为原型
}
}
参数装饰器
参数装饰器只有一个作用,就是判断某个参数是否被传入,下面是一个例子,用来检测可选参数是否被传入,并且还使用到了方法装饰器,因为需要对原有的逻辑进行扩展,加入对于函数参数的判断,先在原型上定义一个 key 为 required 的元数据,其值为参数的索引,在本🌰中,index 为 1,然后在 validate 装饰器中,改写其方法,先获取到之前定义的参数索引数组,然后与实参 arguments 进行对比,即可进行判定
const paramsKey = Symbol('required')
class Person {
name: string;
constructor(name: string) {
this.name = name
}
@validate()
logName(position: number, @required() prefix?: string) {
console.log(prefix + ": " + this.name);
}
}
// 监视该参数是否被传入
type ParamsDecorator = (target: Object, key: string | symbol, index: number) => void
function required(): ParamsDecorator {
return function (target, key, index) {
const parmasIndexArr: number[] = Reflect.getOwnMetadata(paramsKey, target, key) || []
parmasIndexArr.push(index)
Reflect.defineMetadata(paramsKey, parmasIndexArr, target, key)
}
}
type MethodDecoratorType = (target: Object, key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor | void
function validate(): MethodDecoratorType {
return function (target, key, descriptor) {
const method = descriptor.value
descriptor.value = function () {
const parmasIndexArr = Reflect.getMetadata(paramsKey, target, key)
if (!parmasIndexArr) return
for (let index of parmasIndexArr) {
if (index > arguments.length || arguments[index] === void 0) {
throw new Error('need more params')
}
}
return method.apply(this, arguments)
}
}
}
const p = new Person('jack')
p.logName(1)
一些自己的理解
我认为所有知识,都有一定的规律、证据,比如某种现象的背后,必然存在必要的原因,有一个疑惑如下
target:对于静态成员来说是类的构造函数,对于实例成员来说是原型对象???为什么
Ts 编译后的代码如下
class Person {
name: string;
constructor(name: string) {
this.name = name
}
logName(position: number, prefix?: string) {
console.log(prefix + ": " + this.name);
}
static format() {
}
}
// 编译后的
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
Person.prototype.logName = function (position, prefix) {
console.log(prefix + ": " + this.name);
};
Person.format = function () {
};
return Person;
}());
可以看到,类实际上是一个函数,对于 logName,会挂载到原型上,而对于静态方法 format,则会挂载到其构造函数身上,最后将构造函数返回,所以,虽然外层声明的 Person 也可以访问到 format 方法,但是准确来说,外层的类只是存储了指向其构造函数的引用关系
结论: 所以 target 总是指向被装饰者的所属对象,比如类装饰器,其本质上是对类的构造函数服务,比如属性装饰器,通过在原型中定义一些属性,当实例发生访问时,就可以命中原型中的 trigger