本系列旨在帮你用正确的方式打开TypeScript。
前言
这篇内容主要包含了类和TS装饰器的相关用法和实践。
那么这里为什么要专门讲一下class呢?一方面,一些程序员会很喜欢面向对象的编程风格,class的使用也确实会带来很多好处;另一方面,今天的主角,TS中的装饰器是基于类的,而装饰器的使用,无疑可以为ts的开发带来很多便利。
很多成熟的ts框架中,都不乏装饰器的使用。而为了了解装饰器如何使用,我们就先来说说类。这有点“为了这点醋,我才包的这盘饺子”的味道了🤣。
系列文章
更实用的TypeScript指南:不墨迹的入门 - 掘金 (juejin.cn)
更实用的TypeScript指南:类和TS装饰器 - 掘金 (juejin.cn)
一个故事带你了解TypeScript程序类型 - 掘金 (juejin.cn)
类(class)
tips:如果对类已经有了了解,可以直接跳到装饰器部分
接下来,将会通过一个实例的完善,尽量简短快速的让你了解类(更多内容可以访问标题上的链接)
一个简单的类和使用
首先我们创建一个类,并用这个类来实现一个常用的功能(例如设置text)
class SetText {
//类的成员
text:string
//类的构造方法
constructor (str: string) {
this.text = str
}
telltext(): void {
console.log(this.text)
}
}
let setText = new SetText('abc') //创建一个类的实例(其实是个对象)
setText.telltext() //telltext用来输出class里text成员
>> 'abc'
这里new一个实例的过程,简单来说就是新建了一个空对象,将类的成员绑定到对象的属性上,将类的方法绑定到对象的__proto__ 上。
类的继承和supper
接下来,我们让一个派生类去继承基类,并重写父类构造函数,实现text合并的方法
class JoinText extends SetText {
textCom:string
constructor(str1:string, str2:string) {
super(str1)
//继承后的派生类的构造方法必须要有super调用
this.textCom = str1 + str2
}
telltext():void {
console.log(this.textCom)
}
}
let setText = new JoinText('abc','efg')
console.log(setText)
>> JoinText { text: 'abc', textCom: 'abcefg' }
setText.telltext()
>> 'abcefg'
这里的super调用会执行基类的构造函数 ,目的就是为了让派生类继承基类的对象。所以super是必要的
私有属性
类的私有生成字段需要用#前缀来注明,这样实例化出来的对象无法直接访问到这个属性,只能通过getter,setter来取值和设置
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
getter,setter
我们创建两个特殊的‘方法’来获取或设定对象的一些值
class JoinText extends SetText {
textCom:string
constructor(str1:string, str2:string) {
super(str1)
this.textCom = str1 + str2
}
telltext():void {
console.log(this.textCom)
}
get getStr1():string {
return `${this.text}`
}
set setStr1(str2:string) {
this.text = str2
}
}
let setText = new JoinText('abc','efg')
setText.setStr1 = 'xxx'
console.log(setText.getStr1)
>> 'xxx'
虽然这里直接使用setText.text进行赋值效果是一样的,但目的是为了了解setter和getter的用法。重要的是,调用get,set时,并不是调用方法,而是作为属性来调用
static
最后,我们添加一个static成员,他是可以直接用类调用的,同时也可以被static方法调用。
class JoinText extends SetText {
static from:string = 'SetText'
textCom:string
constructor(str1:string, str2:string) {
super(str1)
this.textCom = str1 + str2
}
telltext():void {
console.log(this.textCom)
}
}
console.log(JoinText.from)
>> 'SetText'
至此,我们快速地了解了类的用法,接下来进入正题:装饰器的使用
装饰器
一句话概括:装饰器是用来在类的各个部分添加和设置各种东西的。
装饰器分为类装饰器,成员装饰器,方法装饰器,参数装饰器;分别放在一个类的前面,类的某个成员前面,方法的前面,方法的某个参数的前面。
我们先看一个实例(nestjs的一个控制器)(nest是一个以express为基础的typescript服务器框架)
@Controller('cats') //类装饰器
export class CatsController {
@Get('/hello')
@HttpCode(200)//方法装饰器
recall(@Req()/*参数装饰器*/ request: Request): string {
console.log(request) //将会输出请求的内容
return 'hello nestjs'
}
}
看不懂没关系,我们一步步的来解释每一句:
首先@Controller('cats'),它是一个类装饰器,要放在class的前面,他的作用是定义一个基本的控制器,即添加了一个路由。也就是说现在,这个类成为了一个控制器,具有了一个/cats的路由。
@Get('/hello')和@HttpCode(200)都是方法装饰器,它们放在类的方法前面,他们让这个类具有了更多的功能,分别是增加了一个/hello的子路由和一个返回200的状态码
@Req()是一个参数装饰器,他放在类中函数的参数前面,在这个实例中,他让这个参数具有了请求的参数的意义。
最终运行的结果是这样的:
可以看到,在加这些装饰器之前,这个类其实仅有一个方法:
class CatsController {
recall(request: Request):string {
console.log(request)
return 'hello nestjs'
}
}
而这些装饰器,则给这个类增加了很多功能,从而达到了一个控制器( 处理传入的请求和向客户端返回响应)的作用。这就是装饰器在实际开发中的用法,通过高度的封装,更加快速的实现功能。同时也具备了OOP(面向对象开发)的特点。
基本使用
那么装饰器,是如何实现这些事情的呢?
首先,我们通过实现一个具有提示功能的类来了解类装饰器的用法
初始化:
class Toast{
type:string
tell(text:string):void {
}
}
类装饰器
现在这个类是什么功能都没有的。我们先添加一个类装饰器,使这个类具有一个tiltle
const Description:ClassDecorator = (target) => { //注1
target.prototype.title = '我是一个Toast' //注2
}
@Description //装饰类Toast
class Toast{
type:string
tell(text:string):void {
}
}
let toast = new Toast()
interface NewToast extends Toast{title:string}//注3
console.log((toast as NewToast).title)
>> '我是一个Toast'
- 注1:我们定义了一个Description装饰器,他的类型是
ClassDecorator(类装饰器型)。注意,装饰器是一类函数,不同的装饰器接收的参数不同,这里的类装饰器接收的参数target即为它装饰的类本身。我们在类装饰器中,通过给类本身的prototype上添加属性,从而达到操作类的目的。 - 注2:
target.prototype.title = '我是一个Toast'操纵原型,相当于在class中添加了一个title = '我是一个Toast'的成员,对原型不太了解的童鞋可以评论区评论,我会单独出一期关于原型的文章 - 注3:这里我们定义了一个interface接口,并且继承了Toast和增添了title属性。为的就是通过断言让ts知道toast是有title的。不然会报错,因为这个类本来是没有title成员的。(另外也可以直接使用toast['title'],这样将不进行检查)
通过类装饰器,我们让一个类有了title。但是现在这个title是固定的,如果我们需要传进一个参数来设置title,这时就可以利用装饰器工厂来实现这件事情。
装饰器工厂
装饰器工厂说起来好像比较高大上,我个人理解就是在各种的装饰器外套上一个函数,以达到接收参数的作用🤣。
例如我们将上面的类装饰器改成一个装饰器工厂:
const Description = (title:string):ClassDecorator => { //注1
return (target) => {
target.prototype.title = title
}
}
@Description('装饰器鸭装饰器📢')//注2
class Toast{
type:string
tell(text:string):void {
}
}
let toast = new Toast()
interface NewToast extends Toast{title:string}
console.log((toast as NewToast).title)
>> '装饰器鸭装饰器📢'
- 注1:装饰器工厂将类装饰器套在了一个函数中,并返回了它。所以这里函数的返回是一个类装饰器型。它像是一个普通函数一样使用,只不过返回了一个类装饰器。并没有那么神秘
- 注2:由于装饰器工厂是一个函数调用后返回装饰器,所以这里我们在使用时就直接调用,并且写入参数。
通过类装饰器,我们完成了自定义类的title的功能
成员装饰器
接下来,我们添加一个成员装饰器,来装饰类的type,表示这个提示功能是个警告(warning)。
const initType:PropertyDecorator = (target, propertyKey) => { //注1
target[propertyKey] = 'warning' //注2
}
@Description('装饰器鸭装饰器📢')
class Toast{
@initType//注3
type:string
tell(text:string):void {
}
}
let toast = new Toast()
console.log(toast.type)
>> 'warning'
- 注1:成员装饰器同样是函数,它接收的参数分别为当前类(target)和它装饰的成员的名字(propertyKey)
- 注2:这里的写法就相当于
toast.type = warning - 注3:成员装饰器装饰谁哪个成员把谁放到哪个成员上面
这里自定义type也可以使用装饰器工厂,只不过返回的类型成了方法装饰器。写法大同小异,有兴趣的童鞋可以试一下。
方法装饰器
最后我们加上一个方法装饰器来装饰方法,让他打印出提示信息:
const initTell:MethodDecorator = (target, propertyKey, descriptor) => { //注1
(descriptor.value as unknown) = (text: string):void => { //注2
console.log('warning!这里出了些问题:',text)
}
descriptor.writable = false
// 这种写法ES5之后是没作用的
// target[propertyKey] = (text: string):void => {
// console.log('warning!这里出了些问题:',text)
// }
}
@Description('装饰器鸭装饰器📢')
class Toast{
@initType
type:string
@initTell
tell(text:string):void {
}
}
let toast = new Toast()
toast.tell('按钮')
>>'warning!这里出了些问题: 按钮'
- 注1:方法装饰器的参数分别是:接收类原型对象,对于静态方法来说是类的构造函数(target);方法名;方法的属性描述符 ,这里的属性描述符我们经常用到
descriptor.value和descriptor.writable
- 注2:descriptor.value指向函数,我们可以利用它来重写函数
- 注3:descriptor.writable = false时,实例就没办法用
toast.tell = ...来改写函数了
参数装饰器
参数装饰器的样例如下:
//参数装饰器
const initText:ParameterDecorator = (target, key, index) => {
}
- 注:掺入的参数分别是原型(target),方法名(key),和参数在参数集合中的位置(index,0开始)
它常常和其他的装饰器一同使用。
不看也无所谓的Tips:
类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
可以看看的Tips:
相同级别装饰器是从上往下加载,但是是从下往上调用。即若有几个功能重复的同级别装饰器,最上面的装饰器功能会覆盖掉下面的装饰器
总结
通过对类和装饰器的统一概述,大体介绍了它的用法和作用。回过头看一下最开始给出的一个栗子,是不是思路清晰很多呢?之后,至少不应该在nestjs或者Angular这样的框架时,遇到装饰器不知道是怎么回事了叭🤣
另一方面,装饰器,类这种写法,其实是面向对象的编程特色。一些功能,使用原型链或者class都可以完成的事,以后可以尝试使用class来操作。熟悉面向对象的编程风格,不仅有助于开阔思维,听说还有助于找到对象哦🧐
对了,如果你喜欢这个系列,不妨关注我,一起学习,一起进步!