前言
以前用Nest来写过简单的后台系统,但是都没怎么用到TS,对它里面用到的大量的装饰器也只是一知半解,仅仅停留在了会用的层面。这次实习回到学校了开始准备为毕设做打算了,打算用Nest来写后台,所以现在有时间了打算再回来把装饰器的概念给重新梳理一遍。不管以后用不用Nest,我觉得装饰器也是必须得学习的一个东西,目前装饰器在JS中也是处于Stage2(草案阶段,提供一个初始的草案规范,与最终标准中包含的特性不会有太大差别。草案之后,原则上只接受增量修改。开始实验如何实现,实现形式包括polyfill, 实现引擎(提供草案执行本地支持),或者编译转换(例如babel)
,在TS中则作为实验特性来进行支持,所以这也是JS未来发展的一个方向。 这里也有TS官方对于装饰器的描述。
装饰器概念
它是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为。 通俗的讲装饰器就是一个函数/方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。 常见的装饰器有:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 参数装饰器
装饰器的写法:普通装饰器(无法传参)、装饰器工厂(可传参)
类装饰器
类装饰器在类声明之前被声明〈紧靠着类声明)。类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。传入一个参数
普通装饰器:不能传参
/**
* 装饰器
* target属性就是使用装饰器的那个类
*/
function logClass(target: any) {
target.prototype.apiUrl = 'http://www.baidu.com'
target.prototype.hello = () => {
console.log("hello world")
}
}
@logClass
class HttpClient {
constructor() { }
}
const http: any = new HttpClient()
console.log(http.apiUrl) // http://www.baidu.com
http.hello() //hello world
装饰器工厂:可以传参
/**
* 装饰器工厂
* params就是我们要传递的参数
* target就是要使用装饰器的那个类
*/
function logClass(params: string) {
return function (target: any) {
target.prototype.hello = () => {
console.log(params)
}
}
}
@logClass('hello world')
class HttpClient {
constructor() { }
}
const http: any = new HttpClient()
http.hello() //打印hello world
重载构造函数
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明
function logClass(target: any) {
return class extends target {
apiUrl: string = '修改后的apiUrl'
getData() {
console.log('修改:', this.apiUrl)
}
}
}
@logClass
class HttpClient {
public apiUrl: string | undefined
constructor() {
this.apiUrl = '没修改前的apiUrl'
}
getData() {
console.log(this.apiUrl)
}
}
const http = new HttpClient()
http.getData() //修改: 修改后的apiUrl
属性装饰器
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 装饰的实例。对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 装饰的属性名
/**
* 属性装饰器
* params就是装饰器传入的参数
* target就是装饰的实例
* attr就是装饰的属性
*/
function logProperty(params: any) {
return function (target: any, attr: string) {
//通过这样的方式就可以通过装饰器来修改属性值
target[attr] = params
}
}
class HttpClient {
@logProperty('属性装饰器赋值')
public apiUrl: string | undefined
constructor() {
}
getData() {
console.log(this.apiUrl)
}
}
const http = new HttpClient()
http.getData() // 属性装饰器赋值
方法装饰器
它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。方法装饰会在运行时传入下列个参数:
- 装饰的实例。对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 成员的名字
- 成员的属性描述符
/**
* params 传递给装饰器的值
* target 装饰器的实例
* methodName 方法名称
* descriptor 描述
*/
function get(params: any) {
console.log(params) // http://www.baidu.com
return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
console.log(target)
console.log(methodName)
console.log(descriptor)
//修改前保存原始传入的方法
let originalMethod = descriptor.value
//重写传入的方法
descriptor.value = function (...args: any[]) {
//执行原来的方法
originalMethod.apply(this, args)
args = args.map(val => +val)
console.log(args)
}
}
}
class HttpClient {
constructor() {
}
@get('http://www.baidu.com')
getApi() {
}
}
const http: any = new HttpClient()
http.getApi('123', '456', '789') //打印[123, 456, 789]
方法参数装饰器
运行时会被当做函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入3个参数:
- 装饰的实例。对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
- 方法名
- 参数在函数参数列表中的索引
function logParams(param: any) {
return function (target: any, methodName: string, paramIndex: number) {
console.log(target) // httpClient实例
console.log(methodName) // getApi
console.log(paramIndex) // 0
}
}
class HttpClient {
constructor() {
}
getApi(@logParams('id') id: number) {
console.log(id)
}
}
const http = new HttpClient()
http.getApi(123456)
装饰器的执行顺序
这里先放结论,具体的代码请往下看:
- 属性 > 方法 > 方法参数 > 类
- 如果有多个同样的装饰器,它会先执行后面的(从下到上,方法参数装饰器执行顺序是从右到左)
// 先进行一些装饰器的定义
function logClass1(target: any) {
console.log('logClass1')
}
function logClass2(target: any) {
console.log('logClass2')
}
function logAttribute1(param?: any) {
return function (target: any, attrName: string) {
console.log('attribute1')
}
}
function logAttribute2(param?: any) {
return function (target: any, attrName: string) {
console.log('attribute2')
}
}
function logMethod1(param?: any) {
return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
console.log('logMethod1')
}
}
function logMethod2(param?: any) {
return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
console.log('logMethod2')
}
}
function logParam1(param?: any) {
return function (target: any, methodName: string, index: number) {
console.log('logParam1')
}
}
function logParam2(param?: any) {
return function (target: any, methodName: string, index: number) {
console.log('logParam2')
}
}
@logClass1
@logClass2
class HttpClient {
@logAttribute1()
api1: string | undefined
@logAttribute2()
api2: string | undefined
constructor() {
}
@logMethod1()
get1() {
}
@logMethod2()
get2() { }
get3(@logParam1() param1: string, @logParam2() param2: string) { }
}
上述代码最终的打印结果如下,可以验证了我们一开始得出的执行顺序的结论
attribute1
attribute2
logMethod1
logMethod2
logParam2
logParam1
logClass2
logClass1
结论
装饰器允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能,可以提高代码的复用性,同时减少代码量。