装饰器
在ts中实现装饰器有许多种方法, 如
- 原生js实现
- Reflect反射实现
- ...其他
最近在掘金上看到有人写了一些比较实用的属性装饰器, 这里尝试用不同的方式实现下
1. Default
该装饰器在属性没有值的时候, 为其默认赋值, 如下
class foo {
@Default('name is foo')
name!: string;
}
const foo = new Foo()
console.log(foo.name) // name is foo即默认实例化的时候, 属性 name 有一个默认值 name is foo
1.1 原生js实现
function ComDefault(value: unknown) {
return function (target: Object, propertyKey: string | symbol) {
Object.defineProperty(target, propertyKey, {
configurable: true,
enumerable: true,
writable: true,
value: value
});
};
}1.2 Reflect反射实现
1.2.1 简单实现
function RefDefault(value: unknown) {
return function (target: Object, propertyKey: string | symbol) {
Reflect.set(target, propertyKey, value);
};
}1.2.2 Reflect metadata实现
Reflect metadata还有很多其他api, 也比较复杂, 后面找机会再总结下看看
类定义
class Foo {
name!: string;
}装饰器
import 'reflect-metadata'
const classDecorator: PropertyDecorator = (target: any, propertyKey) => {
target[propertyKey] = 'classDecorator';
}
const demo = new Foo()
console.log('开始', demo.name) // undefined
Reflect.decorate([classDecorator], demo, 'name')
console.log('结束', demo.name) // classDecorator在上面主要是使用了Reflect metadata 中的 decorate 方法, 方法有多种重载, 可以对类/属性 /方法分别进行处理, 具体如下
/**
* 应用于一个装饰器的集合给目标对象
* @param decorators 装饰器的数组
* @param target 目标对象
* @returns 返回应用提供的装饰器后的值
* 注意:装饰器应用是与array的位置方向相反, 为从右往左
*/
function decorate(decorators: ClassDecorator[], target: Function): Function;
/**
* 应用一个集合去装饰目标对象的方法
* @param 装饰器的集合
* @param 目标对象
* @param 要装饰的属性名称 key
* @param 该属性的描述 descriptor
*/
function decorate(decorators: (PropertyDecorator | MethodDecorator)[], target: Object, propertyKey: string | symbol, attributes?: PropertyDescriptor : PropertyDescriptor;2. Alias
该装饰器在为实例赋值的时候, 取其他字段的值给当前字段, 如下
class foo {
@RefAlias('detail')
name!: string;
}
const res = {
detail: 'not empty'
}
const foo = new Foo();
Object.assign(foo, res);
console.log(foo.name) // not empty即赋值的时候, 将属性detail 的值赋给属性 name , 最后就打印得到 not empty
具体实现(原生js和Reflect的实现差不多, 就放一起了)
function RefAlias(name: string) {
return function (target: any, propertyKey: string) {
Object.defineProperty(target, name, {
set(value) {
// target[propertyKey] = value
Reflect.set(target, propertyKey, value)
}
})
}
}3. 使用Ts实现控制反转和依赖注入
之前看过一点java的学习资料, 当时对这个控制翻转就不是很清楚, 这次看了这篇文章, 同时也工作了两年, 终于也能明白一点了.
3.1 阶段一: 强耦合
export class Flower {
strew() {
console.log("strew flower!!!");
}
}
export class Hello {
private readonly flower: Flower;
constructor() {
this.flower = new Flower();
}
greet(name: string) {
console.log(`hello ${name}!`);
this.flower.strew();
}
}在此阶段, 类Hello 的构造函数由Flower 组成, 如果类Flower 的构造函数有变更, 那类Hello 的构造函数也需要进行改变, 这样两者之间就有耦合了.
3.2 阶段二: 去耦合
export class Flower {
public name: string;
constructor(name: string) {
this.name = name
}
strew() {
console.log("strew flower!!!");
}
}
export class Hello {
private readonly flower: Flower;
constructor(f: Flower) {
this.flower = f;
}
greet(name: string) {
console.log(`hello ${name}!`);
this.flower.strew();
}
}在此阶段, 如果类Flower 发生变化, 那么类Hello 内部的构造函数不用进行变化, 但是在外部实例化的时候依然需要先实例化Flower
const flower = new Flower('flower')
const hello = new Hello(flower)3.3 阶段三: 控制反转
// 全局容器
class Container {
bindMap = new Map()
bind(identifier: string, clazz: any, args: Array<any>) {
this.bindMap.set(identifier, {
clazz,
args,
})
}
get(identifier: string) {
const target = this.bindMap.get(identifier)
if(!target) {
throw new Error(`instance ${identifier} has not be bind`)
}
const { clazz, args } = target
return Reflect.construct(clazz, constructorArgs)
}
}
// 具体使用
export class Flower {
public name: string;
constructor(name: string) {
this.name = name
}
strew() {
console.log("strew flower!!!");
}
}
export class Hello {
private readonly flower: Flower;
constructor() {
this.flower = container.get('flower');
}
greet(name: string) {
console.log(`hello ${name}!`);
this.flower.strew();
}
}
const container = newContainer()
container.bind('flower', Flower, 'flower')
container.bind('hello', Hello)
// 从容器中取出
const hello = container.get('hello');
console.log(hello.flower) // { flower: { name: 'flower' } }在此阶段使用一个全局容器 container` 来维护所有的类实例, 在需要的时候再取出来.
但是这样的话依然需要开发者在构造函数中手动指定所需要的函数, 也需要开发者手动创建实例.
上面代码中, Reflect.construct 方法为: 通过传递构造函数或类以及相应的参数来创建对象的实例.
方法接受以下参数:
target:表示要实例化的目标函数或类。argumentsList:一个类似数组的对象,包含传递给目标构造函数或类的参数列表。newTarget(可选):可指定一个新的构造函数或类作为执行上下文,用来创建实例
3.4 阶段四 依赖注入
需要开启
"emitDecoratorMetadata": true
import 'reflect-metadata'
// 全局容器
const providerMap = new WeakMap()
// 装饰器, 声明的时候就注册实例到全局容器
function provider(target: any) {
providerMap.set(target, null)
}
// 构造实例的辅助函数, 通过该函数可以通过递归的方式依次构建实例(无论构造参数是否是类)
function create(target: any) {
// 获取目标类的构造参数
const paramsType = Reflect.getOwnMetadata('design:paramtypes', target) || []
// 递归查找构造参数, 查找后返回并创建一个实例
const deps = paramsType.map((type: any) => {
const instance = providerMap.get(type)
if (instance === null) {
providerMap.set(type, create(type))
}
return providerMap.get(type)
})
return new target(...deps)
}
@provider
class FlowerService {
public age: number | undefined
constructor(age: number) {
this.age = age
}
name() {
console.log('this is flower')
}
}
@provider
class Hello {
public flower: FlowerService | undefined
public detail: string | undefined
constructor(flower: FlowerService, detail: string) {
this.flower = flower
}
}
const hello = create(Hello)
hello.flower.name() // this is flower
console.log(hello.detail) // undefined在上面的代码里面使用到了装饰器自动注入的design:paramtypes 来获取参数的构造参数, 然后对构造参数进行遍历获取, 获取到后就进行依次创建, 如果里面有普通参数, 那该参数就是undefined, 如下所示
[undefined] [class FlowerService]
[FlowerService { age: undefined }, undefined] [class Hello]此外, 上面还有一点需要注意的是, create 函数最后构造实例的时候不能选择Reflect.construct , 查了一下, 下面是AI的回复
Reflect.construct和new构造在大多数情况下是等效的,它们都用于实例化构造函数生成对象。然而,在某些情况下它们可能会有细微的差异。下面是一些可能导致
Reflect.construct和new构造不相等的情况:
- 异常处理:当构造函数抛出异常时,
new构造将直接抛出错误并中断执行,而Reflect.construct方法会返回一个代理对象并将异常存储在代理对象的[[ConstructError]]属性中。- 参数传递:使用
new构造时,构造函数的参数是以逗号分隔的列表方式传递的,而Reflect.construct方法可以通过传入参数数组的方式来传递构造函数的参数。- 原型链的 constructor:使用
new构造生成的实例的原型链上的constructor属性总是指向原始构造函数本身,但使用Reflect.construct生成的实例的原型链上的constructor属性则根据构造函数的原型链进行设置。综上所述,尽管
Reflect.construct和new构造在大多数情况下是等效的,但由于异常处理、参数传递方式和原型链的差异,它们可能在少数特殊情况下产生不同的行为。
应该主要还是原型链的问题, 导致最后生成的实例不能继承构造函数的原型
