浅谈 typescript 装饰器

浅谈 typescript 装饰器

typescript 的装饰器,是个很尴尬的存在,从这个概念被提出以来,就一直在经历各种规范上的修改,直到现在还没有一种公认、稳定的规范拿上台。

装饰器是和 class 的概念相关的,从尤大的话来说,前端页面的交互逻辑使用 class 来写,可能并不是那么顺手。从 vue3 放出的 api 来看,vue3 最终还是选择了开放函数式编程的接口。用尤大的话来讲:设计一个 class 编程接口,要踩的坑实在是太多。

不过即使是冷门的语法,也阻止不了 好奇得闲 的我去 科研玩耍 一下。

什么是装饰器

说到装饰器,我们首先联想到的是 css。在编写 html 文档时,我们使用 css 写好页面的样式,然后通过 css 选择器将这些样式绑定到 html 标签。

typescript 的装饰器也是类似的使用方法,编写一个装饰器函数,然后通过 @methodName 将这个函数绑定到类、类的属性、类的方法。

它们的使用方法,有点像是做好一朵小红花,然后别在某个小朋友的衣领上一样,顾名思义装饰器。

类属性装饰器

typescript 的属性装饰器,是指对类中的属性进行装饰,以代码入侵性较低的方式增强该属性。当我们使用类对象的属性时,完成我们自定义的额外功能。

下面是一个用 es5 风格编写的属性装饰器的例子,通过 Object.defineProperty 将类的普通属性改写成存取器属性,然后通过 Object accessorgetter and setter 拦截属性的基础操作,从而添加自定义的额外代码。

属性装饰器函数有固定的参数:target 指被修饰的类,key 指被修饰类的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//类属性装饰器
function logProperty(target:any,key:string){
delete target[key]
let backing = '_'+key

Object.defineProperty(target,backing,{
writable:true,
configurable:true,
enumerable:true
})

function getter(this:any){
const value = this.backing
console.log(`Get: ${key} => ${value}`)
return value
}

function setter(this:any,newVal:any){
console.log(`Set: ${key} => ${newVal}`)
this.backing = newVal
}

Object.defineProperty(target,key,{
get:getter,
set:setter,
configurable:true,
enumerable:true
})
}

class Employee{
@logProperty
name?:string
}

let e = new Employee
e.name//"Get: name => undefined"
e.name = 'sugar'//"Set: name => sugar"
e.name//"Get: name => sugar"

我们使用 es6 风格的 Proxy 和 Reflect 重写一下看看,哈哈,我也不会写,毕竟我也是抄别人的。

类装饰器

类装饰器可以给一个类插入一个类方法。

我们创建一个什么都不写的空类:Greeter,然后使用类装饰器为这个空类添加一个方法:greet()

类装饰器只需要一个参数:target,指代被装饰的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//类装饰器
function Greeting(target:any){
target.prototype.greet = function(){
console.log(`我是greet方法哒,没想到吧 !!`)
}
}
}

@Greeting
class Greeter{

}

let greeter = new Greeter()
greeter.greet()

如果我们的装饰方法需要传递参数,那么可以使用下面的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//类装饰器
function Greeting(greeting:string){
return function(target:any){
target.prototype.greet = function(){
console.log(`我是greet方法哒,没想到吧! ${greeting} !!`)
}
}
}

@Greeting('乾杯 - ( ゜- ゜)つロ')
class Greeter{

}

let greeter = new Greeter()
greeter.greet()

类方法装饰器

方法装饰器和属性装饰器相比,多了一个参数:descriptor,这个参数里记录了原函数的引用,用于调用原函数。

下面的例子中,我们通过改写 descriptor.value 将原来的方法替换成了新方法,并新增了日志功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//类方法装饰器
function LogMethod(target:any,key:string,descriptor:any){
const oldMethod = descriptor.value
const newMethod = function(this:any,...args:any[]){
const result = oldMethod.call(this,args)
if(!this.logOutput){
this.logOutput = new Array<any>()
}
this.logOutput.push({
method:key,
parameter:args,
result:result,
timestamp:new Date()
})
}
descriptor.value = newMethod
}

class Calculator {
@LogMethod
double(num:number){
return num * 2
}
}

let c = new Calculator()
c.double(2)
c.double(10)
console.log(c.logOutput)
/*
[LOG]: [{
"method": "double",
"parameter": [
2
],
"result": 4,
"timestamp": "2021-07-21T08:58:45.706Z"
}, {
"method": "double",
"parameter": [
10
],
"result": 20,
"timestamp": "2021-07-21T08:58:45.706Z"
}]
*/

结语

以上是3种常用装饰器的使用案例,typescript 还有其他装饰器,这里就不列出了,因为笔者也只了解这3种。

本篇没有对装饰器做出足够详细的讲解,笔者也只懂这些,不足之处请多多包涵。