一、TS中类的装饰器
类的装饰器特性:
- 类的装饰器:对类进行修饰的工具
- 装饰器本身是一个函数
- 装饰器通过@符号来使用
- 装饰器的运行时刻(时机):创建(定义)类的时候,就会立即对类进行装饰,而不是对类进行实例化的时候
- 类装饰器接收的参数是构造函数
- 当有多个装饰器时,会从左到右,从上到下收集所有的装饰器。装饰器执行顺序:从下到上,从右到左。
环境搭建
npm init -y
tsc --init // 创建tsconfig.json文件
npm install ts-node -D
npm install typescript --save
// package.json
"scripts": {
"dev": "ts-node ./src/index.ts"
},
-
简单装饰器
function testDecorator(constructor: any) { console.log('decorator') } @testDecorator class Test { } // 装饰器定义类的时候就会立即执行装饰器函数,对类进行装饰 const test = new Test() // new实例化的时候并不会执行装饰器函数
上述代码提示如下
提示装饰器是一项实验性的属性。 在tsconfig.json打开"experimentalDecorators": true,"emitDecoratorMetadata": true,对试验类型的支持属性就不会有此提示了。
-
多个装饰器的用法:
function testDecorator(constructor: any) { console.log('decorator') } function testDecorator1(constructor: any) { console.log('decorator') } @testDecorator @testDecorator1 class Test { }
如上所述:当有多个装饰器时,会从左到右,从上到下收集所有的装饰器。装饰器执行顺序:从下到上,从右到左。
- 工厂模式的包装
factory装饰器
3.1 语法提示不够完整
// 工厂模式 在一定条件下执行
function testDecorator(flag: boolean) {
if (flag) {
return function (constructor: any) {
constructor.prototype.getName = () => {
console.log('dell')
}
}
} else {
return function (constructor: any) { }
}
}
@testDecorator(true) // 通过传入的参数控制装饰器
class Test { }
const test = new Test();
(test as any).getName()
3.2 增加的方法在原来的类上没有
// 工厂模式优化
function testDecorator<T extends new (...args: any[]) => {}>(constructor: T) {
return class extends constructor {
// 2.后执行
name = 'bro' // 将传入的name覆盖
getName() {
return this.name
}
}
}
@testDecorator
class Test {
name: string
constructor(name: string) {
// 1.先执行 扩展机制
this.name = name
}
}
const test = new Test('fruit');
console.log((test as any).getName())
// test.getName() 报错,是因为Test这个实例上本身没有getName方法,TS无法识别出来,是通过装饰器加上的
(...args: any[])含义:可以接收任意多个参数,将所有的参数合并到数组中。 new (...args: any[]) => {}代表一个构造函数,返回值为对象。 执行顺序,下执行Test实例中的constructor,再执行装饰器中的constructor
3.2 用装饰器修饰原来的类,使增加的方法在原来的类上可以直接访问
// 优化写法
function testDecorator() {
return function <T extends new (...args: any[]) => {}>(constructor: T) {
return class extends constructor {
name = 'bro'
getName() {
return this.name
}
}
}
}
const Test = testDecorator()(
class {
name: string
constructor(name: string) {
this.name = name
}
}
)
const test = new Test('fruit');
test.getName() // 此时不会报错,如下图
原因:现在的Test是装饰器装饰过后的类,已经装饰过后的类,Test上就有getName方法了,因此就可以识别了。
二、TS中的方法装饰器
普通方法,target对应的事类的prototype
方法构造器接收三个参数target、key、descriptor
执行顺序:类的装饰器会在定义类的时候,立马执行。方法装饰器在类创建好之后,立即对方法做修改
静态方法static,target对应的是类的构造函数
function getNameDecorator(target: any, key: string) {
console.log(target, key)
}
class Test {
name: string
constructor(name: string) {
this.name = name
}
@getNameDecorator
static getName() {
return 'this.name'
}
}
参考文档: developer.mozilla.org/zh-CN/docs/…
接收三个参数Object.defineProperty(obj, prop, descriptor)
我们可以对getName方法进行修改
const test = new Test('fruit')
test.getName = () => { // 对getName方法做修改
return '123'
}
如何让getName方法不做修改
function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false // 设置为false,方法则不可被修改,如getName
descriptor.value = function() { // value为属性或方法的原始值,
return 'decorator'
}
}
function getNameDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
// console.log(target, key)
descriptor.writable = false // // 设置为false,方法则不可被修改,如getName
descriptor.value = function () { // value为属性或方法的原始值,
return 'decorator'
}
}
class Test {
name: string
constructor(name: string) {
this.name = name
}
@getNameDecorator
getName() {
return this.name
}
}
const test = new Test('fruit')
console.log(test.getName()) // decorator
总结:装饰器对一个方法做完装饰之后,可以对target(原型)、key、descriptor都可以拿到,对方法可以做很多你想要的修改,这就是类class中方法装饰器的作用。
三、TS中访问器的装饰器
访问器:getter 、setter
访问器的装饰器的使用方法
function visitDecorator(target: any, key: string, descriptor: PropertyDescriptor) {}
class Test {
private _name: string
constructor(name: string) {
this._name = name
}
get name() { // getter访问器
return this._name
}
@visitDecorator // 给访问器增加装饰器
set name(name: string) { // setter访问器
this._name = name
}
}
const test = new Test('fruit')
console.log(test.name) // fruit
test.name = '123'
console.log(test.name) // 123
增加descriptor.writable = false
function visitDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false // 此访问器是不可修改的
}
...
test.name = '123' // 不能被重写
报错如下:
如果同时在setter和getter上用了相同的装饰器,就会报错。
报错原因:TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。简而言之:上述代码中name方法实际为一个属性,在setter写装饰器和在getter上写装饰器最终的效果都是一样的,两个上边都写,就会造成重复。
错误原因参考文档:www.tslang.cn/docs/handbo…
四、TS中的属性装饰器
- 属性装饰器也是
Decorator的写法,接收两个参数target(原型)、key(属性的名字),可以通过返回descriptor来替换掉属性原始的descriptor。 - 使用属性装饰器无法直接修改属性。修改的实际是原型的属性值。而类定义的属性是直接存储在类的实例上的。因此,修改原型上的内容并不会对实例上的属性有任何的变更影响。
下面是对以上两点的具体解释:
属性装饰器没有descriptor描述器
// 属性装饰器
function nameDecorator(target: any, key: string) {
console.log(target, key)
}
class Test {
@nameDecorator
name = 'fruit'
}
const test = new Test()
console.log(test.name) // fruit
打印结果如下
创建descriptor,并返回,此时修改name属性就会报错
function nameDecorator(target: any, key: string): any {
// 创建descriptor,并返回,会替换原始的descriptor
const descriptor: PropertyDescriptor = {
writable: false
}
return descriptor
}
class Test {
@nameDecorator
name = 'fruit' // 报错
}
const test = new Test()
错误如下:
把这段代码在TS官网上运行编译:
function nameDecorator(target: any, key: string): any {
target[key] = 'bro'
}
class Test {
@nameDecorator
name = 'fruit' // 报错
}
const test = new Test()
console.log(test.name) // 输出为fruit,为什么不是bro呢?
输出为fruit,为什么不是bro呢?
如上图:Test的name属性是在constructor构造器上的,而装饰器修改的是prototype上的name属性。修改的根本不是一个东西。而在运行时,先会找实例上的name属性,实例上有name属性,就不会再找prototype上的name属性了。实例上没有name属性的时候,才会找扩展的target的原型上的name属性。
// 修改的并不是实例上的name,而是原型上的name
function nameDecorator(target: any, key: string): any {
target[key] = 'bro'
}
// name放在实例上
class Test {
@nameDecorator
name = 'fruit' // 报错
}
const test = new Test()
console.log((test as any).__proto__.name) // 通过__proto__属性就可以访问到prototype上的name属性
因此,想要使用属性装饰器,直接对属性的值做修改,实际上是做不到的。
五、TS中的参数装饰器
参数装饰器包含3个参数:
-
target: 原型
-
method: 方法名
-
paramIndex: 参数所在的位置下标
// target: 原型 method: 方法名 paramIndex: 参数所在的位置下标 function paramDecorator(target: any, method: string, paramIndex: number) { console.log(target, method, paramIndex) //Test { getInfo: [Function] } 'getInfo' 0 } class Test { getInfo(@paramDecorator name: string, age: number) { console.log(name, age) // fruit 18 } } const test = new Test() test.getInfo('fruit', 18)
六、TS中装饰器的使用例子
const userInfo: any = undefined
class Test {
getName() {
try {
return userInfo.name
} catch (error) {
console.log('userInfo.name 不存在')
}
}
getAge() {
try {
return userInfo.age
} catch (error) {
console.log('userInfo.age 不存在')
}
}
}
const test = new Test()
test.getName() // userInfo.name 不存在
如果用上述方法来处理异常,代码就会异常臃肿。我们可以用装饰器来解决。
方法一:普通方法,使用key来获取错误的方法名,但是错误提示不够明确,不能说明是userInfo.name或userInfo.age有问题,只能说明此方法有问题
function catchError(target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value // 对应的方法
descriptor.value = function () {
try {
fn()
} catch (error) {
console.log(`userInfo ${key} error`)
}
}
}
const userInfo: any = undefined
class Test {
@catchError
getName() {
return userInfo.name
}
@catchError
getAge() {
return userInfo.age
}
}
const test = new Test()
test.getName() // userInfo getName error
test.getAge() // userInfo getAge error
方法二: 使用工厂模式
function catchError(msg: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value
descriptor.value = function () { // 重写方法
try {
fn() // 先执行原方法
} catch (error) {
console.log(msg)
}
}
}
}
const userInfo: any = undefined
class Test {
@catchError('userInfo.name error')
getName() {
return userInfo.name
}
@catchError('userInfo.age error')
getAge() {
return userInfo.age
}
}
const test = new Test()
test.getName() // userInfo.name error
test.getAge() // userInfo.age error
七、reflect-metadata
reflect-metadata可以帮助我们在类或类的属性上存储一些元数据,并且方便的进行数据的反射和获取。
什么是元数据:就是类上面存储的一些额外的数据。元数据挂载对象上,但直接打印又看不到的形态,想做到这点就可以用reflect-metadata来帮助我么 参考链接:reflect-metadata
npm install reflect-metadata --save
import 'reflect-metadata'
const user = {
name: 'fruit'
}
Reflect.defineMetadata('data', 'test', user)
console.log(user) // { name: 'fruit' }
最基础的定义元数据,获取元数据方法
import 'reflect-metadata'
const user = {
name: 'fruit'
}
Reflect.defineMetadata('data', 'test', user) // 赋值
console.log(Reflect.getMetadata('data', user)) // 取值 test
在类上定义元数据
import 'reflect-metadata'
@Reflect.metadata('data', 'test') // 赋值
class User {
name = 'dell'
}
console.log(Reflect.getMetadata('data', User)) // test
在类的属性或方法上定义元数据
// 在类的属性上定义元数据
import 'reflect-metadata'
class User {
@Reflect.metadata('data', 'test') // 在类的属性上定义
name = 'dell'
}
// 挂载在了类的原型链上
console.log(Reflect.getMetadata('data', User.prototype, 'name')) // test
// 在类的方法上定义元数据
import 'reflect-metadata'
class User {
@Reflect.metadata('data', 'test')
getName() { }
}
console.log(Reflect.getMetadata('data', User.prototype, 'getName')) // test
常用API:
hasMeatdata:判断当前的target(对象)上有没有对应的元数据
// 使用hasMeatdata
class User {
@Reflect.metadata('data', 'test')
getName() { }
}
console.log(Reflect.hasMetadata('data', User.prototype, 'getName')) // true
hasOwnMetadata:是否拥有改属性
mport 'reflect-metadata'
class User {
@Reflect.metadata('data', 'test')
getName() { }
}
class Teacher extends User { } // 继承
console.log(Reflect.hasMetadata('data', User.prototype, 'getName')) // true
console.log(Reflect.hasMetadata('data', Teacher.prototype, 'getName')) // true
console.log(Reflect.hasOwnMetadata('data', User.prototype, 'getName')) // true
console.log(Reflect.hasOwnMetadata('data', Teacher.prototype, 'getName')) // false
由于Teacher是的data是继承而来的,因此为false.
getMetadataKeys:获取类的方法上的元数据的名字有哪些
import 'reflect-metadata'
class User {
@Reflect.metadata('data', 'test')
getName() { }
}
console.log(Reflect.getMetadataKeys(User.prototype, 'getName'))
结果如下:
'design:returntype','design:paramtypes','design:type',为默认自身的类型
getOwnMetadataKeys: 和getOwnMetadata类似,
import 'reflect-metadata'
class User {
@Reflect.metadata('data', 'test')
getName() { }
}
class Teacher extends User { }
console.log(Reflect.getOwnMetadataKeys(User.prototype, 'getName'))
console.log(Reflect.getOwnMetadataKeys(Teacher.prototype, 'getName'))
打印结果如下:Teacher的getOwnMetadataKeys为空,继承的是没有自身的元数据的。
deleteMetadata:当我们在一个方法上定义了metadata元数据后,可以通过deleteMetadata进行删除
在TS中利用reflect-metadata元数据的定义和反射获取元数据的机制,可以对我们的代码做哪些改良?
八、TS中装饰器的执行顺序
不同的装饰器结合在一起应该如何使用?他们的执行顺序是怎么样的?他们怎么和元数据做关联?
function showData(target: typeof User) {
for (let key in target.prototype) {
const data = Reflect.getMetadata('data', target.prototype, key)
console.log(data)
}
}
import 'reflect-metadata'
@showData
class User {
@Reflect.metadata('data', 'name')
getName() { }
@Reflect.metadata('data', 'age')
getAge() { }
}
打印结果如上图,说明方法的装饰器执行一定优先于类的装饰器,否则将无法获取到方法装饰器的值。
封装类似装饰器setData,可以非常灵活的进行个性化定制:
function showData(target: typeof User) {
for (let key in target.prototype) {
const data = Reflect.getMetadata('data', target.prototype, key)
console.log(data)
}
}
function setData(dataKey: string, msg: string) {
return function (target: User, key: string) {
Reflect.defineMetadata(dataKey, msg, target, key)
}
}
import 'reflect-metadata'
@showData
class User {
@Reflect.metadata('data', 'name')
getName() { }
@setData('data', 'age')
getAge() { }
}
打印结果如下,和第一个例子一致,说明使用Reflect.defineMetadata自定义的装饰器效果正常:
- 方法装饰器优先于类的装饰器执行,类的装饰器是最后执行的。
Reflect.metadata的原理性,就是使用Reflect.defineMetadata来实现的。
推荐学习网站:
- www.typescriptlang.org/ TS官网,查看对应官方文档
- ts-ast-viewer.com/# 帮助我们将语法变成抽象语法书,进一步分析写的内容会转化为什么
- redux.js.org/recipes/usa… 在redux中如何使用TS,在redux官网搜索
typescript即可.