装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。
装饰器使用 @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!');
}
@helloWord
class 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
@g
x
带参数的装饰器
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); // 18
let hello = new Hello();
console.log(hello.name); // xiaoyu
console.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 = x
this._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 {
@parseFunc
public addOne(@parse('number') num: any) {
console.log('num:', num)
return num + 1
}
}
// 在函数调用前执行格式化操作
function parseFunc(target: any, name: any, descriptor: any) {
const originalMethod = descriptor.value
descriptor.value = function(arg: any) {
switch (parseType) {
case 'number':
arg = Number(arg)
break
case 'string':
arg = String(arg)
break
case 'boolean':
arg = String(arg) === 'true'
break
}
return originalMethod.call(this, arg)
}
return descriptor
}
// 向全局对象中添加对应的格式化信息
// type可选为 number, string, boolean
function parse(type: string) {
return function(target: Function, name: string, index: number) {
parseType = type
}
}
let modalStr = new Modal()
console.log(modalStr.addOne('10')) // 11
let modalNum = new Modal()
console.log(modalNum.addOne(20)) // 21
let modalBool = new Modal()
console.log(modalBool.addOne(true)) // 2
属性装饰器
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
function log(target: any, propertyKey: string) {
let value = target[propertyKey]
// 用来替换的getter
const getter = function() {
console.log(`Getter for ${propertyKey} returned ${value}`)
return value
}
// 用来替换的setter
const 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 {
@log
public num!: number;
square() {
return this.num * this.num
}
}
let cal = new Calculator()
cal.num = 2
console.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 decorator
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am class decorator
从上述例子得出如下结论:
- 有多个参数装饰器时:从最后一个参数依次向前执行
- 方法和方法参数中参数装饰器先执行。
- 类装饰器总是最后执行。
- 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。
上述例子中属性和方法调换位置,输出如下结果:
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am property decorator
I am class decorator