装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。
装饰器使用 @expression 这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑。
个人认为装饰器是一种解决方案,而并非是狭义的@Decorator,后者仅仅是一个语法糖罢了。
装饰器的类型有:类装饰器、访问器装饰器、属性装饰器、方法装饰器、参数装饰器,但是没有函数装饰器(function)。
装饰器执行时机
修饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
启用装饰器
若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{"compilerOptions": {"target": "ES5","experimentalDecorators": true}}
定义装饰器
装饰器本身其实就是一个函数,理论上忽略参数的话,任何函数都可以当做装饰器使用。
function helloWord(target: any) {console.log('hello World!');}@helloWordclass HelloWordClass {}new HelloWordClass() // 输出 `hello World!`
其中 target 就是目标类的构造函数。
编译结果(ES5):
"use strict";var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, 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;};function helloWord(target) {console.log('hello World!');}var HelloWordClass = /** @class */ (function () {function HelloWordClass() {}HelloWordClass = __decorate([helloWord], HelloWordClass);return HelloWordClass;}());new HelloWordClass(); // 输出 `hello World!`
装饰器组合
多个装饰器可以同时应用到一个声明上,就像下面的示例:
书写在同一行上:
@f @g x
书写在多行上:
@f@gx
带参数的装饰器
function Path(p1: string, p2: string) {return function (target: any) { // 这才是真正装饰器console.log(`${p1} ${p2}`);}}@Path("hello", "world")class HelloService {}new HelloService()
类装饰器
应用于类构造函数,其参数是类的构造函数。
注意class并不是像Java那种强类型语言中的类,而是JavaScript构造函数的语法糖。
function addAge(args: number) {return function(target: Function) { // target 为 Function(构造函数)target.prototype.age = args;};}@addAge(18)class Hello {name: string;age!: number;constructor() {this.name = 'xiaoyu'}}console.log(Hello.prototype.age); // 18let hello = new Hello();console.log(hello.name); // xiaoyuconsole.log(hello.age); // 18
方法装饰器
它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰会在运行时传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符{value: any, writable: boolean, enumerable: boolean, configurable: boolean}。
function method(type: string) {return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {console.log(type)console.log('prop ' + propertyKey)console.log('desc ' + JSON.stringify(descriptor) + '\n\n')}}class Hello {name: string;age: number;constructor() {this.name = 'xiaoyu'this.age = 18}@method('instance')hello() {return 'instance method'// instance// prop hello// desc {"writable":true,"enumerable":false,"configurable":true}}@method('static')static run() {return 'static method'// static// prop run// desc {"writable":true,"enumerable":false,"configurable":true}}}
访问器装饰器
访问器装饰器应用于访问器的属性描述符,可用于观察,修改或替换访问者的定义。 访问器装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类中)。
TypeScript不允许为单个成员装饰get和set访问器。相反,该成员的所有装饰器必须应用于按文档顺序指定的第一个访问器。这是因为装饰器适用于属性描述符,它结合了get和set访问器,而不是单独的每个声明。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
如果代码输出目标版本小于ES5返回值会被忽略。
下面是使用了访问器装饰器(@configurable)的例子,应用于Point类的成员上:
function configurable(value: boolean) {return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {descriptor.configurable = value}}class Point {private _x: number;private _y: number;constructor(x: number, y: number) {this._x = xthis._y = y}@configurable(false)get x() { return this._x }set x(value: number) {this._x = value}@configurable(false)get y() { return this._y }set y(value: number) {this._y = value}}
方法参数装饰器
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 参数的名字。
- 参数在函数参数列表中的索引。
let parseType: string = ''class Modal {@parseFuncpublic addOne(@parse('number') num: any) {console.log('num:', num)return num + 1}}// 在函数调用前执行格式化操作function parseFunc(target: any, name: any, descriptor: any) {const originalMethod = descriptor.valuedescriptor.value = function(arg: any) {switch (parseType) {case 'number':arg = Number(arg)breakcase 'string':arg = String(arg)breakcase 'boolean':arg = String(arg) === 'true'break}return originalMethod.call(this, arg)}return descriptor}// 向全局对象中添加对应的格式化信息// type可选为 number, string, booleanfunction parse(type: string) {return function(target: Function, name: string, index: number) {parseType = type}}let modalStr = new Modal()console.log(modalStr.addOne('10')) // 11let modalNum = new Modal()console.log(modalNum.addOne(20)) // 21let modalBool = new Modal()console.log(modalBool.addOne(true)) // 2
属性装饰器
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
function log(target: any, propertyKey: string) {let value = target[propertyKey]// 用来替换的getterconst getter = function() {console.log(`Getter for ${propertyKey} returned ${value}`)return value}// 用来替换的setterconst setter = function(newVal: any) {console.log(`Set ${propertyKey} to ${newVal}`)value = newVal}// 替换属性,先删除原先的属性,再重新定义属性if (delete target[propertyKey]) {Object.defineProperty(target, propertyKey, {get: getter,set: setter,enumerable: true,configurable: true})}}class Calculator {@logpublic num!: number;square() {return this.num * this.num}}let cal = new Calculator()cal.num = 2console.log(cal.square())// Set num to 2// Getter for num returned 2// Getter for num returned 2// 4
装饰器加载顺序
function ClassDecorator() {return function(target: any) {console.log('I am class decorator')}}function MethodDecorator() {return function(target: any, methodName: string, descriptor: PropertyDescriptor) {console.log('I am method decorator')}}function Param1Decorator() {return function(target: any, methodName: string, paramIndex: number) {console.log('I am parameter1 decorator')}}function Param2Decorator() {return function(target: any, methodName: string, paramIndex: number) {console.log('I am parameter2 decorator')}}function PropertyDecorator() {return function(target: any, propertyName: string) {console.log('I am property decorator')}}@ClassDecorator()class Hello {@PropertyDecorator()greeting!: string;@MethodDecorator()greet(@Param1Decorator() p1: string, @Param2Decorator() p2: string) { }}
输出结果:
I am property decoratorI am parameter2 decoratorI am parameter1 decoratorI am method decoratorI am class decorator
从上述例子得出如下结论:
- 有多个参数装饰器时:从最后一个参数依次向前执行
- 方法和方法参数中参数装饰器先执行。
- 类装饰器总是最后执行。
- 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。
上述例子中属性和方法调换位置,输出如下结果:
I am parameter2 decoratorI am parameter1 decoratorI am method decoratorI am property decoratorI am class decorator
