装饰器(decorator) 装饰器是一种特殊的声明, 可以附加在类、方法、访问器、属性、参数声明上
装饰器使用@expression
的形式,其中expression必须能够演算为运行时调用的函数,其中包含装饰声明信息.
装饰器类型及其执行顺序为
类装饰器—优先级 4 (对象实例化, 静态)
方法装饰器—优先级 2 (对象实例化, 静态)
访问器或属性装饰器—优先级 3 (对象实例化, 静态)
参数装饰器—优先级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 function f ( ) { console .log("f(): evaluated" ); return function (target, propertyKey: string , descriptor: PropertyDescriptor ) { console .log("f(): called" ); } } function g ( ) { console .log("g(): evaluated" ); return function (target, propertyKey: string , descriptor: PropertyDescriptor ) { console .log("g(): called" ); } } class C { @f () @g () method ( ) {} }
f
和g
返回了另一个函数(装饰器函数). f
和g
称为装饰器工厂
装饰器工厂
帮助用户传递可供装饰器利用的参数
我们可以看到装饰器的,演算规则
为由顶向上 ,执行顺序由底向上
装饰器的签名 1 2 3 4 5 6 7 8 9 10 11 12 13 interface TypedPropertyDescriptor<T> { enumerable?: boolean ; configurable?: boolean ; writable?: boolean ; value?: T; get?: () => T; set?: (value: T ) => void ; } declare type ClassDecorator = <TFunction extends Function > (target: TFunction) => TFunction | void;declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
方法装饰器 可以从上面的签名中,看到它有三个参数
target: 当前对象的原型,也就是说,假设Employee是对象,那么target就是Employee.prototype
propertyKey: 方法的名称
descriptor: 方法的属性描述符,即Object.getOwnPropertyDescriptor(Employ.prototype, propertyKey)
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 export function logMethod ( target: Object , propertyName: string , propertyDescriptor: PropertyDescriptor ): PropertyDescriptor { const method = propertyDesciptor.value; propertyDesciptor.value = function (...args: any [] ) { const params = args.map(a => JSON .stringify(a)).join(); const result = method.apply(this , args); const r = JSON .stringify(result); console .log(`Call: ${propertyName} (${params} ) => ${r} ` ); return result; } return propertyDesciptor; }; class Employee { constructor (private firstName: string , private lastName: string ) {} @logMethod greet(message: string ): string { return `${this .firstName} ${this .lastName} says: ${message} ` ; } } const emp = new Employee('Mohan Ram' , 'Ratnakumar' );emp.greet('hello' );
javascript 编译后是什么样
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 var __decorate = (this && this .__decorate) || function (decorators, target, key, desc ) { var c = arguments .length; var r = c < 3 ? target : desc === null ? (desc = Object .getOwnPropertyDescriptor(target, key)) : desc; var d; if ( typeof Reflect === "object" && typeof Reflect .decorate === "function" ) { r = Reflect .decorate(decorators, target, key, desc); } else { for (var i = decorators.length - 1 ; i >= 0 ; i--) { if ((d = decorators[i])) { r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; } } } return c > 3 && r && Object .defineProperty(target, key, r), r; }; var Employee = (function ( ) { function Employee (firstName, lastName ) { this .firstName = firstName; this .lastName = lastName; } Employee.prototype.greet = function (message ) { return this .firstName + " " + this .lastName + " says: " + message; }; __decorate([logMethod], Employee.prototype, "greet" ); return Employee; })(); var emp = new Employee("Mohan Ram" , "Ratnakumar" );emp.greet("hello" );
让我们开始分析Employee
函数—构造器初始化name
和greet
方法,将其传入原型.
1 __decorate([logMethod], Employee.prototype, "greet" );
这是 TypeScript 自动生成的通用方法,它根据装饰器类型和相应参数处理装饰器函数调用。
该函数有助于内省方法调用,并为开发者铺平了处理类似日志
、记忆化
、应用配置
等横切关注点的道路。
在这个例子中,我们仅仅打印了函数调用及其参数、响应。
注意,阅读 __decorate
方法中的详细注释可以理解其内部机制。
属性装饰器 属性装饰器有两个参数
target: 当前对象原型, 也就说,假设Employee是对象,那么target就是Employee.prototype
propertyKey: 属性名称
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 logParameter (target: Object , propertyName: string ) { let _val = this [propertyName]; const getter = () => { console .log(`Get: ${propertyName} => ${_val} ` ); return _val; }; const setter = newVal => { console .log(`Set: ${propertyName} => ${newVal} ` ); _val = newVal; }; if (delete this [propertyName]) { Object .defineProperty(target, propertyName, { get: getter, set: setter, enumerable: true , configurable: true }); } } class Employee { @logParameter name: string ; } const emp = new Employee();emp.name = 'Mohan Ram' ; console .log(emp.name);
上面的代码中, 我们在装饰器中内省属性的可访问性, 下面是编译后的代码
1 2 3 4 5 6 7 8 9 10 11 var Employee = (function ( ) { function Employee ( ) { } __decorate([ logParameter ], Employee.prototype, "name" ); return Employee; }()); var emp = new Employee();emp.name = 'Mohan Ram' ; console .log(emp.name);
参数装饰器 参数装饰器函数有三个参数:
target: 当前对象的原型,也就是说,假设Employee是对象, 那么 target就是Employee.prototype
propertyKey: 参数名称
index: 参数数组中的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function logParameter (target: Object , propertyName: string , index: number ) { const metadataKey = `log_${propertyName} _parameters` ; if (Array .isArray(target[metadataKey])) { target[metadataKey].push(index); } else { target[metadataKey] = [index]; } } class Employee { greet(@logParameter message: string ): string { return `hello ${message} ` ; } } const emp = new Employee();emp.greet('hello' );
上面的代码中,我们收集了所有被装饰的方法的索引或位置, 作为元数据加入对象的原型.后面是编译后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var __param = (this && this .__param) || function (paramIndex, decorator ) { return function (target, key ) { decorator(target, key, paramIndex); } }; var Employee = (function ( ) { function Employee ( ) {} Employee.prototype.greet = function (message ) { return "hello " + message; }; __decorate([ __param(0 , logParameter) ], Employee.prototype, "greet" ); return Employee; }()); var emp = new Employee();emp.greet('hello' );
类似之前见过的__decorate函数,__param 函数返回一个封装参数装饰器的装饰器
如我们所见,调用参数装饰器时, 会忽略其返回值.这就意味着,调用__param
函数时, 其返回值不会用来覆盖参数值
这就是参数装饰器
不反回的原因
访问器装饰器 访问器不过是类声明中属性的读取访问器和写入访问器
访问器装饰器应用于访问器的属性描述符
,可用于观测
、修改
、替换
访问器的定义
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 enumerable (value: boolean ) { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { console .log('decorator - sets the enumeration part of the accessor' ); descriptor.enumerable = value; }; } class Employee { private _salary: number; private _name: string; @enumerable(false ) get salary () { return `Rs. ${this ._salary} ` ; } set salary (salary: any ) { this ._salary = +salary; } @enumerable(true ) get name () { return `Sir/Madam, ${this ._name} ` ; } set name (name: string ) { this ._name = name; } } const emp = new Employee();emp.salary = 1000 ; for (let prop in emp) { console .log(`enumerable property = ${prop} ` ); }
上面的例子中,我们定义了两个访问器name
和salary
,并通过装饰器设置是否将其列入清单,据此决定对象的行为.name
将入清单,而salary
不会
下面是编译的代码
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 enumerable (value ) { return function (target, propertyKey, descriptor ) { console .log('decorator - sets the enumeration part of the accessor' ); descriptor.enumerable = value; }; } var Employee = (function ( ) { function Employee ( ) { } Object .defineProperty(Employee.prototype, "salary" , { get: function ( ) { return "Rs. " + this ._salary; }, set: function (salary ) { this ._salary = +salary; }, enumerable: true , configurable: true }); Object .defineProperty(Employee.prototype, "name" , { get: function ( ) { return "Sir/Madam, " + this ._name; }, set: function (name ) { this ._name = name; }, enumerable: true , configurable: true }); __decorate([ enumerable(false ) ], Employee.prototype, "salary" , null ); __decorate([ enumerable(true ) ], Employee.prototype, "name" , null ); return Employee; }()); var emp = new Employee();emp.salary = 1000 ; for (var prop in emp) { console .log("enumerable property = " + prop); }
类装饰器 类装饰器应用于类的构造器,可用于观测、修改、替换类的定义
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 export function logClass (target: Function ) { const original = target; function construct (constructor, args ) { const c: any = function ( ) { return constructor .apply (this , args ); } c.prototype = constructor .prototype ; return new c(); } const f: any = function (...args ) { console .log(`New: ${original['name' ]} is created` ); return construct(original, args); } f.prototype = original.prototype; return f; } @logClass class Employee {}let emp = new Employee();console .log('emp instanceof Employee' );console .log(emp instanceof Employee);
上面的装饰器声明了一个名为original的变量,将其值设为装饰的类构造器.
接着声明了一个名为construct的辅助函数,该函数用于创建类的实例.
接下来创建了一个名为f的变量,该变量用于用作新的构造器,该函数调用原构造器,同时在控制台打印了实例话的类名
这正是我们给原构造器加入的额外的行为的地方 原来构造器的原型复制到了f,以确保创建一个Employee新实例的时候,instanceof操作符的效果符合预期
新构造器一旦就绪,我们便返回它, 以完成类构造器的实现
新构造器就绪后,每次创建实例时会在控制台打印类名
编译后的代码如下
1 2 3 4 5 6 7 8 9 10 11 var Employee = (function ( ) { function Employee ( ) { } Employee = __decorate([ logClass ], Employee); return Employee; }()); var emp = new Employee();console .log('emp instanceof Employee' );console .log(emp instanceof Employee);
在编译后的代码中,我们注意到两处不同:
传给__decorate的参数有两个, 装饰器数组和构造器函数
TypeScript编译器使用__decorate的返回值以覆盖原构造器
这正是类装饰器必须返回一个构造函数的原因所在
装饰器工厂 由于每种装饰器都有它自身的调用签名,我们可以使用装饰器工厂来泛化装饰器调用
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 import { logClass } from './class-decorator' ;import { logMethod } from './method-decorator' ;import { logProperty } from './property-decorator' ;import { logParameter } from './parameter-decorator' ;export function log (...args ) { switch (args.length) { case 3 : if typeof args[2 ] === "number" ) { return logParameter.apply(this , args); } return logMethod.apply(this , args); case 2 : return logProperty.apply(this , args); case 1 : return logClass.apply(this , args); default : throw new Error ('Not a valid decorator' ); } } @log class Employee { @log private name: string ; constructor (name: string ) { this .name = name; } @log greet(@log message: string ): string { return `${this .name} says: ${message} ` ; } }
元信息反射API 元信息反射API(例如 Reflect)能够用来以标准的方式组织元信息.
[反射]的意思是代码可以侦测同一系统中的其他代码(或其自身).
反射在组合/依赖注入、运行时类型断言、测试等使用场景下很有用.
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 import "reflect-metadata" ;export function logParameter (target: Object , propertyName: string , index: number ) { const indices = Reflect .getMetadata(`log_${propertyName} _parameters` , target, propertyName) || []; indices.push(index); Reflect .defineMetadata(`log_${propertyName} _parameters` , indices, target, propertyName); } export function logProperty (target: Object , propertyName: string ): void { var t = Reflect .getMetadata("design:type" , target, propertyName); console .log(`${propertyName} type: ${t.name} ` ); } class Employee { @logProperty private name: string ; constructor (name: string ) { this .name = name; } greet(@logParameter message: string ): string { return `${this .name} says: ${message} ` ; } }
上面的代码用到了reflect-metadata
这个库.其中我们使用了反射元信息的设计键(例如design:type
).目前只有三个:
类型元信息: design:type
,
参数类型元信息: design:paramtypes
返回类型元信息: design:returntype
,
有了反射.我们就能够在运行时得到一下信息:
实体名
实体类型
实体实现的接口
实体构造器参数的名称和类型
总结
装饰器 不过是在设计时帮助内省代码,注解及修改类和属性的函数
Yehuda Katz 提议在ECMAScript2016标准中加入装饰器特征tc39/proposal-decorators
我们可以通过装饰器工厂将用户提供的参数传给装饰器
有4种装饰器: 类装饰器、方法装饰器、属性/访问器装饰器、参数装饰器
元信息反射API有助于以标准化的方式在对象中加入元信息,以及在运行时获取设计类型信息
备注 本文源于
https://juejin.im/post/6844903876605280269
代码https://github.com/mohanramphp/typescript-decorators