装饰器这个名词,如果不是写angular、nest的其他前端同学应该都不怎么熟悉,简单来说,装饰器就是函数,提供了某种特定的功能,用于描述类、方法、属性、参数,为其添加更加强大的功能,同时与原有逻辑进行解耦,算是aop编程的一种实现。
或者说可能在平时有用过一些
例如react中使用redux的时候,有用过@connect
@connect(mapStateToProps, mapDispatchToProps)
class Index extends React.Component{}
在vue2中,使用过vue-property-decorator插件
@Component({})
class Index extends Vue {}
今天,我就来带大家看看装饰器到底是个什么东西。
javascript中的装饰器提案
javascript是有装饰器这个提案的,但是迟迟无法落地。
最早的时候,装饰器的提案与现在不同,而Typescript早早实现了装饰器,正好angular2彻底使用了Typescript来重构,大量使用了在当时还是提案的装饰器。
但是到现在,装饰器的提案早已与当时不同,被改的面目全非,在这种前提下,angular团队以及后来的nest团队,肯定是不同意新的装饰器提案的。就像当时的Promise A+,也是社区推动,但是在官方实现Promise之前,社区已经有使用了Promise的库,为了兼容这些库,现在对于Promise的判断,现在都是基于thenable这种鸭子类型来进行判断,而不是通过instanceof这种更为精准的从底层进行判断的。
与Promise不同,Promise是双重控制反转,重点在于执行顺序,具体如何实现,其实并不是很重要的,人们更在意的是它的用处。而装饰器,是实实在在的代码逻辑层面的,更改某个规则,就意味着整体逻辑可能是完全不一样的。强行推行,对于之前使用angular、nest的项目,完全是破坏性的打击,angular团队和nest团队,已经Typescript团队,在tc39上肯定是不愿意通过提案的。这也就导致了装饰器提案的持续搁置。
不过说来也有意思,一般来说,提案被否决之后,都需要重新回到stage1从头来过,但是装饰器却一直在stage2。
使用装饰器
在官方没有实现这个提案之前,我们要使用装饰器,通常有两种做法
- 使用babel插件
- 使用Typescript
之前说过,Typescript团队,在很早之前就实现了装饰器的功能,因此我们只需要创建一个.ts文件,就可以自由的使用装饰器了,当然,要开启experimentalDecorators选项。
装饰器工厂
在介绍装饰器之前,先简单介绍一个概念——装饰器工厂。
顾名思义,工厂是用来进行组装的地方,装饰器工厂也就是用来组装某些值以及要装饰的东西的。
与普通的装饰器函数相比,它多了一层调用,用于传递要组装的数据,因此装饰器工厂与普通装饰器最大的差别就是它的自定义参数。
类装饰器
类装饰器,声明在class关键字上方。
简单理解,就是将这个类,作为装饰器的参数传递进去,在装饰器函数中,可以对这个类进行各种操作。
废话不多说,直接来看代码
@Init
class Index {
public age = 12
}
function Init<T extends {new (...args: any[]): {}}>(constructor: T) {
return class extends constructor {
age = 21
}
}
console.log(new Index())
// class_1 { age: 21 }
// function Init<T extends new (...args: any[]) => {}>(constructor: T): {
// new (...args: any[]): (Anonymous class);
// prototype: Init<any>.(Anonymous class);
// } & T
在实例化这个Index类的时候,同时会调用它的装饰器Init,并将Index传递进去,在此基础上,我们就可以通过这个函数对类进行各种操作。
下面我们来看看类装饰器的装饰器工厂,怎么使用,也就是平时用的@connect这种的方法
@InjectSex('男')
class Two {}
function InjectSex(sex: '男' | '女') {
return function<T extends {new (...args: any): {}}>(target: T) {
target.prototype.sex = sex
return target
}
}
console.log(Reflect.getPrototypeOf(new Two()))
// { sex: '男' }
方法装饰器
方法装饰器是用于修饰方法的,与类装饰器只有一个target参数不同,方法装饰器共接收三个参数,分别是
- target 类实例
- key 方法的名字
- descriptor用于描述这个方法的描述符,也就是Object.defineProperty方法的第三个参数中的value、writable、enummerable、configurable
class Fun {
@AddOne
log(x: number) {
console.log(x)
}
}
function AddOne(target, key, descriptor) {
console.log(target, 'target') // { log: [Function (anonymous)] } target
console.log(key, 'key') // log key
console.log(descriptor, 'descriptor')
// {
// value: [Function (anonymous)],
// writable: true,
// enumerable: true,
// configurable: true
// } descriptor
const val = descriptor.value
descriptor.value = function(...args) {
return val(args[0] + 1)
}
return descriptor
}
const fun = new Fun
fun.log(1)
// 2
我们通过descriptor中的value属性,劫持到原有的方法,并进行重新改写,这样就可以以最小的切入面修改一个现有的方法了。
如果是装饰器工厂的话,我们还是需要在外面包裹一层函数
class FuncTwo {
@InjectPrefix('托尼-')
log(x) {
console.log(x)
}
}
function InjectPrefix(prefix: string) {
return function(target, key, descriptor) {
const val = descriptor.value
descriptor.value = function(...args) {
return val(prefix + args[0])
}
return descriptor
}
}
const funcTwo = new FuncTwo
funcTwo.log('斯塔克')
// 托尼-斯塔克
属性装饰器
属性装饰器一般用于属性的劫持,它接收两个参数,分别是target和当前属性的名称,我们可以通过装饰器工厂来向被装饰的属性添加值。
class Prop {
@init(16)
age: number
}
function init(age: number) {
return function(target, key) {
target[key] = age
return target
}
}
const prop = new Prop
console.log(prop.age)
// 16
参数装饰器
参数装饰器接收三个参数,分别是target、key(当前方法)和index(当前参数的下标)
class Param {
log(@require name: string, @require age: number) {
console.log(name, age)
}
}
function require(target, key, index) {
console.log(target, key, index)
return target
}
const param = new Param
param.log('张三', 18)
// { log: [Function (anonymous)] } log 1
// { log: [Function (anonymous)] } log 0
// 张三 18
不过一般都使用方法装饰器来配合其使用,例如下面这个例子
class Param {
@Validate
log(@require name?: string, @require age?: number) {
console.log(name, age)
}
}
function Validate(target, key, descriptor) {
const val = descriptor.value
const required = val.required
console.log(required) // [0, 1]
descriptor.value = function(...args) {
required.forEach(index => {
if (!args[index]) {
throw new Error('缺少参数')
}
})
return val(...args)
}
return descriptor
}
function require(target, key, index) {
target[key].required = [index, ...(target[key].required || [])]
return target
}
const param = new Param
param.log()
// /Users/asarua/Desktop/demo/decorator/params-decorator.ts:13
// required.forEach(index => {
^
// Error: 缺少参数
通过require参数装饰器,向target[key]方法中添加required的参数,然后通过Validate进行校验。
结语
装饰器这个东西,一直都是看java工程师在使用,在每个Controller、Service、还有方法中,加一大堆。
其实前端工程师在日常工作中也可以试试用,在一些需要执行log啥的方法中,使用装饰器,可以更好的将无关逻辑进行解耦,更好的进行维护。
作者:Asarua
链接:https://juejin.cn/post/6965428382284644388
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。