Skip to content

装饰器 ​

https://juejin.cn/post/7173694757078564894#heading-6

在ts中实现装饰器有许多种方法, 如

  • 原生js实现
  • Reflect反射实现
  • ...其他

最近在掘金上看到有人写了一些比较实用的属性装饰器, 这里尝试用不同的方式实现下

1. Default ​

该装饰器在属性没有值的时候, 为其默认赋值, 如下

typescript
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实现 ​

typescript
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 简单实现 ​

typescript
function RefDefault(value: unknown) {
    return function (target: Object, propertyKey: string | symbol) {
        Reflect.set(target, propertyKey, value);
    };
}

1.2.2 Reflect metadata实现 ​

Reflect metadata 还有很多其他api, 也比较复杂, 后面找机会再总结下看看

类定义

typescript
class Foo {
  name!: string;
}

装饰器

typescript
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 方法, 方法有多种重载, 可以对类/属性 /方法分别进行处理, 具体如下

typescript
/**
  * 应用于一个装饰器的集合给目标对象
  * @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 ​

该装饰器在为实例赋值的时候, 取其他字段的值给当前字段, 如下

typescript
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的实现差不多, 就放一起了)

typescript
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 阶段一: 强耦合 ​

typescript
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 阶段二: 去耦合 ​

typescript
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

typescript
const flower = new Flower('flower')
const hello = new Hello(flower)

3.3 阶段三: 控制反转 ​

typescript
// 全局容器
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

typescript
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 构造不相等的情况:

  1. 异常处理:当构造函数抛出异常时,new 构造将直接抛出错误并中断执行,而 Reflect.construct 方法会返回一个代理对象并将异常存储在代理对象的 [[ConstructError]] 属性中。
  2. 参数传递:使用 new 构造时,构造函数的参数是以逗号分隔的列表方式传递的,而 Reflect.construct 方法可以通过传入参数数组的方式来传递构造函数的参数。
  3. 原型链的 constructor:使用 new 构造生成的实例的原型链上的 constructor 属性总是指向原始构造函数本身,但使用 Reflect.construct 生成的实例的原型链上的 constructor 属性则根据构造函数的原型链进行设置。

综上所述,尽管 Reflect.construct 和 new 构造在大多数情况下是等效的,但由于异常处理、参数传递方式和原型链的差异,它们可能在少数特殊情况下产生不同的行为。

应该主要还是原型链的问题, 导致最后生成的实例不能继承构造函数的原型