一、简介 装饰器本质是一种特殊的函数 ,它可以对:类、属性、方法、参数进行扩展,同时能让代码更简洁。 装饰器自2015年在ECMAScript-6中被提出到现在,已将近10年。 截止目前,装饰器依然是实验性特性 ,需要开发者手动调整配置,来开启装饰器支持。 装饰器有 5 种: 1、类装饰器 2、属性装饰器 3、方法装饰器 4、访问器装饰器 5、参数装饰器
备注:虽然TypeScript5.0中可以直接使用**类装饰器**,但为了确保其他装饰器可用,现阶段使用时,仍建议使用experimentalDecorators配置来开启装饰器支持,而且不排除在来的版本中,官方会进一步调整 装饰器的相关语法! 参考:《TypeScript 5.0发版公告》
二、类装饰器 2.1、基本语法 类装饰器是一个应用在类声明 上的函数 ,可以为类添加额外的功能,或添加额外的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Demo (target : Function ) { console .log (target) } @Demo class Person { constructor (public name :string ) {} } new Person ('zhangsan' );
2.2、应用举例 需求:定义一个装饰器,实现Person实例调用toString时返回JSON.stringify的执行结果。
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 function CustomString (target : Function ) { target.prototype .toString = function ( ) { return JSON .stringify (this ); }; Object .seal (target.prototype ) } @CustomString class Person { constructor (public name : string , public age : number ) { } speak ( ) { console .log (`我叫${this .name} ,今年${this .age} 岁了` ); } } const p1 = new Person ("小明" , 15 );console .log (p1.toString ()); interface Person { a : any ; } Person .prototype .a = 100 ;console .log (p1.a )
2.3、关于返回值 类装饰器有返回值 :若类装饰器返回一个新的类,那这个新类将替换 掉被装饰的类。类装饰器无返回值 :若类装饰器无返回值或返回undefined,那被装饰的类不会 被替换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function demo (target :Function ){ return class { test ( ){ console .log (200 ) console .log (300 ) console .log (400 ) } } } @demo class Person { test ( ){ console .log (100 ) } } console .log (Person )
2.4、关于class构造类型 在 TypeScript 中,Function 类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等。但并非Function 类型的函数都可以被 new 关键字实例化,例如箭头函数是不能被实例化的,那么 TypeScript 中概如何声明一个构造类型呢?有以下两种方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 type Constructor = new (...args : any []) => {};function test (fn :Constructor ){}class Person {}test (Person )
1 2 3 4 5 6 7 8 9 10 11 12 type Constructor = { new (...args : any []): {}; wife : string ; }; class Person { static wife = 'asd' } function test (fn :Constructor ){}test (Person )
2.5、替换被装饰的类 对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。
**需求:**设计一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间。
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 interface User { getTime (): void ; } type Constructor = new (...args : any []) => {};function LogTime <T extends Constructor >(target : T) { return class extends target { createdTime : Date ; constructor (...args : any [] ) { super (...args); this .createdTime = new Date (); } getTime ( ) { return `该对象创建时间为:${this .createdTime} ` ; } }; } @LogTime class User { constructor (public name : string , public age : number ) { } speak ( ) { console .log (`${this .name} 说:你好啊!` ); } } const user1 = new User ("张三" , 13 );console .log (user1.getTime ());
三、装饰器工厂 装饰器工厂是一个返回装饰器函数的函数(装饰器工厂中会包含一个装饰器) ,可以为装饰器添加参数,可以更灵活地控制装饰器的行为。
**需求:**定义一个LogInfo类装饰器工厂,实现Person实例可以调用到introduce方法,且introduce中输出内容的次数,由LogInfo接收的参数决定。
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 interface Person { introduce : () => void } function LogInfo (n : number ) { return function (target : Function ) { target.prototype .introduce = function ( ) { for (let i = 0 ; i < n; i++) { console .log (`我的名字:${this .name} ,我的年龄:${this .age} ` ) } } } } @LogInfo (5 ) class Person { constructor ( public name : string , public age : number ) { } speak ( ) { console .log ('你好呀!' ) } } let p1 = new Person ('张三' , 18 )p1.speak () p1.introduce ()
四、装饰器组合 装饰器组合 即装饰器工厂和装饰器的组合
4.1、执行顺序 装饰器可以组合使用,执行顺序为:先【由上到下 】的执行所有的装饰器工厂,依次获取到装饰器,然后再【由下到上 】执行所有的装饰器。(先执行装饰器工厂后执行装饰器,装饰器工厂由上往下,装饰器由下往上执行)
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 function test1 (target : Function ) { console .log ('test1' ) } function test2 ( ) { console .log ('test2工厂' ) return function (target : Function ) { console .log ('test2' ) } } function test3 ( ) { console .log ('test3工厂' ) return function (target : Function ) { console .log ('test3' ) } } function test4 (target : Function ) { console .log ('test4' ) } @test1 @test2 ()@test3 ()@test4 class Person { }
4.2、应用 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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 type Constructor = new (...args : any []) => {}interface Person { introduce (): void getTime (): void } function customToString (target : Function ) { target.prototype .toString = function ( ) { return JSON .stringify (this ) } Object .seal (target.prototype ) } function LogTime <T extends Constructor >(target : T) { return class extends target { createdTime : Date ; constructor (...args : any [] ) { super (...args); this .createdTime = new Date (); } getTime ( ) { return `该对象创建时间为:${this .createdTime} ` ; } }; } function LogInfo (n : number ) { return function (target : Function ) { target.prototype .introduce = function ( ) { for (let i = 0 ; i < n; i++) { console .log (`我的名字:${this .name} ,我的年龄:${this .age} ` ) } } } } @customToString @LogInfo (3 )@LogTime class Person { constructor ( public name : string , public age : number ) { } speak ( ) { console .log ('你好呀!' ) } } const p1 = new Person ('张三' , 18 )console .log (p1.toString ()) p1.introduce () console .log (p1.getTime ())
五、属性装饰器 基本语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Demo (target : object , propertyKey : string ) { console .log (target, propertyKey) } class Person { @Demo name : string @Demo age : number @Demo static school : string constructor (name : string , age : number ) { this .name = name this .age = age } } const p1 = new Person ('张三' , 18 )
关于属性遮蔽 常规逻辑:给实例对象this添加一个age属性,js会先查看是否有这个属性,没有就查看原型对象上是否有。如下代码在给原型对象添加age属性之前就已经new了一个实例对象,因此实例age是18 原型对象的age是130
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 / 常规 class Person { name : string ; age : number ; constructor (name : string , age : number ) { this .name = name; this .age = age; } } const P1 = new Person ('小明' , 18 )let value = 130 Object .defineProperty (Person .prototype , 'age' , { get ( ) { return value }, set (v ) { value = v } }) console .log (P1 );
**属性遮蔽:**如下代码中:当构造器中的this.age = age试图在实例上赋值时,实际上是调用了原型上age属性的set方法。
逻辑:第一次执行到this.age = age的时候。发现没有age就在原型对象找执行get方法这时候发现age是130,往下执行new一个实例对象的时候,因为这时候的age是原型对象中的,因此会执行set函数,将18赋值给age
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person { name : string age : number constructor (name : string , age : number ) { this .name = name this .age = age } } let value = 130 Object .defineProperty (Person .prototype , 'age' , { get ( ) { return value }, set (val ) { value = val } }) const p1 = new Person ('张三' , 18 )console .log (p1)
应用举例 需求:定义一个State属性装饰器,来监视属性的修改。
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 function State (target : object , propertyKey : string ) { let key = `__${propertyKey} ` ; Object .defineProperty (target, propertyKey, { get ( ) { return this [key] }, set (newVal : string ) { console .log (`${propertyKey} 的最新值为:${newVal} ` ); this [key] = newVal }, enumerable : true , configurable : true , }); } class Person { name : string ; @State age : number ; school = 'atguigu' ; constructor (name : string , age : number ) { this .name = name; this .age = age; } } const p1 = new Person ('张三' , 18 );const p2 = new Person ('李四' , 30 );p1.age = 80 p2.age = 90 console .log ('------------------' )console .log (p1.age ) console .log (p2.age )
六、方法装饰器 基本语法 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 function Demo (target : object , propertyKey : string , descriptor : PropertyDescriptor ) { console .log (target) console .log (propertyKey) console .log (descriptor) } class Person { constructor ( public name : string , public age : number , ) { } @Demo speak ( ) { console .log (`你好,我的名字:${this .name} ,我的年龄:${this .age} ` ) } @Demo static isAdult (age : number ) { return age >= 18 ; } } const p1 = new Person ('张三' , 18 )p1.speak ()
应用举例 定义一个Logger方法装饰器,用于在方法执行前和执行后,均追加一些额外逻辑。 定义一个Validate方法装饰器,用于验证数据。 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 function Logger (target : object , propertyKey : string , descriptor : PropertyDescriptor ) { const original = descriptor.value ; descriptor.value = function (...args : any [] ) { console .log (`${propertyKey} 开始执行......` ) const result = original.call (this , ...args) console .log (`${propertyKey} 执行完毕......` ) return result; } } function Validate (maxValue : number ) { return function (target : object , propertyKey : string , descriptor : PropertyDescriptor ) { const original = descriptor.value ; descriptor.value = function (...args : any [] ) { if (args[0 ] > maxValue) { throw new Error ('年龄非法!' ) } return original.apply (this , args); }; } } class Person { constructor ( public name : string , public age : number , ) { } @Logger speak ( ) { console .log (`你好,我的名字:${this .name} ,我的年龄:${this .age} ` ) } @Validate (120 ) static isAdult (age : number ) { return age >= 18 ; } } const p1 = new Person ('张三' , 18 )p1.speak () console .log (Person .isAdult (100 ))
七、访问器装饰器 基本语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function Demo (target : object , propertyKey : string , descriptor : PropertyDescriptor ) { console .log (target) console .log (propertyKey) console .log (descriptor) } class Person { @Demo get address (){ return '北京宏福科技园' } @Demo static get country (){ return '中国' } }
应用举例 需求:对Weather类的temp属性的set访问器进行限制,设置的最低温度-50,最高温度50
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 function RangeValidate (min : number , max : number ) { return function (target : object , propertyKey : string , descriptor : PropertyDescriptor ) { const originalSetter = descriptor.set ; descriptor.set = function (value : number ) { if (value < min || value > max) { throw new Error (`${propertyKey} 的值应该在 ${min} 到 ${max} 之间!` ); } if (originalSetter) { originalSetter.call (this , value); } }; }; } class Weather { private _temp : number ; constructor (_temp : number ) { this ._temp = _temp; } @RangeValidate (-50 , 50 ) set temp (value ) { this ._temp = value; } get temp () { return this ._temp ; } } const w1 = new Weather (25 );console .log (w1)w1.temp = 67 console .log (w1)
八、参数装饰器 基本语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function Demo (target : object , propertyKey : string , parameterIndex : number ) { console .log (target) console .log (propertyKey) console .log (parameterIndex) } class Person { constructor (public name : string ) { } speak (@Demo message1 : any , mesage2 : any ) { console .log (`${this .name} 想对说:${message1} ,${mesage2} ` ); } }
应用举例 需求:定义方法装饰器Validate,同时搭配参数装饰器NotNumber,来对speak方法的参数类型进行限制。
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 function NotNumber (target : any , propertyKey : string , parameterIndex : number ) { let notNumberArr : number [] = target[`__notNumber_${propertyKey} ` ] || []; notNumberArr.push (parameterIndex); target[`__notNumber_${propertyKey} ` ] = notNumberArr; } function Validate (target : any , propertyKey : string , descriptor : PropertyDescriptor ) { const method = descriptor.value ; descriptor.value = function (...args : any [] ) { const notNumberArr : number [] = target[`__notNumber_${propertyKey} ` ] || []; for (const index of notNumberArr) { if (typeof args[index] === 'number' ) { throw new Error (`方法 ${propertyKey} 中索引为 ${index} 的参数不能是数字!` ) } } return method.apply (this , args); }; return descriptor; } class Student { name : string ; constructor (name : string ) { this .name = name; } @Validate speak (@NotNumber message1 : any , mesage2 : any ) { console .log (`${this .name} 想对说:${message1} ,${mesage2} ` ); } } const s1 = new Student ("张三" );s1.speak (100 , 200 );