装饰器这个名词,如果不是写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
啥的方法中,使用装饰器,可以更好的将无关逻辑进行解耦,更好的进行维护。