简介
TS
中的装饰器(Decorator
)是一种特殊的声明,用以对类进行装饰加工。它可以被附加到类的声明,以及类中的成员属性、方法、访问器、方法参数、构造器参数上。
TS
对装饰器提供了官方支持,它作为一个实验性功能,可以在 tsconfig.json
中将 compilerOptions.experimentalDecorators
设置为 true
开启。
装饰器的本质是函数,比如一个装饰器名为 decorator
,则以 @decorator
的形式使用。装饰器对类的改变是在代码编译时发生的,编译后的代码中,定义被装饰的类时,装饰器函数会被调用,被装饰的内容相关的数据会作为参数传入装饰器函数中。
装饰器基本使用
以作用于类本身的装饰器为例:
- 接收一个参数
target
,表示被装饰的类本身。
- 可以返回一个新的类以覆盖被装饰的类,如果不显式返回,则默认返回被装饰的类本身。
示例:
function substitute(target: any) {
console.log(target); // 输出被装饰的类本身
return class Substitute {
sayHello() {
console.log("Hello Decorator");
}
};
}
@substitute
class Person {
sayHello() {
console.log("Hello Class");
}
}
const person = new Person();
person.sayHello(); // 输出:"Hello Decorator"
装饰器工厂
装饰器支持以装饰器工厂的形式使用。装饰器工厂本质是一个用以返回一个装饰器的工厂函数。比如一个装饰器工厂名为 decoratorFactory
,则以 @decoratorFactory(args)
的形式使用,这样会将调用 decoratorFactory(args)
返回的装饰器应用到目标上。装饰器工厂使得可以通过传入不同的参数而快速生成不同的装饰器。
同一目标使用多个装饰器
此处使用一个工厂函数 marker
传入一个数字作为参数生成装饰器,装饰器会将该数字赋值给被装饰的类的静态属性 index
。分别将该 marker
生成的不同装饰器以及另一个装饰器 breaker
添加给 Person
类:
function marker(n: number) {
console.log("装饰器工厂 marker 被调用,参数是 " + n);
return function (target: any) {
console.log("装饰器函数被调用,其标记的数字为 " + n);
target.index = n;
};
}
function breaker(target: any) {
console.log("我是来破坏队形的");
return target;
}
@marker(1)
@marker(2)
@breaker
@marker(3)
class Counter {
static index: number;
}
console.log("被装饰的类最后得到的静态属性 index 的值为 " + Counter.index);
/* 最终控制台输出如下:
装饰器工厂 marker 被调用,参数是 1
装饰器工厂 marker 被调用,参数是 2
装饰器工厂 marker 被调用,参数是 3
装饰器函数被调用,其标记的数字为 3
我是来破坏队形的
装饰器函数被调用,其标记的数字为 2
装饰器函数被调用,其标记的数字为 1
被装饰的类最后得到的静态属性 index 的值为 1
*/
可以看出,当对同一个目标使用多个装饰器时,如果使用的是装饰器工厂,则它们会按代码中的顺序从上到下执行得到装饰器,而装饰器本身会按从下到上的顺序执行。
作用于不同目标的装饰器
除了作用于类本身,也可以对类中的其它目标使用装饰器,对于不同的目标,有时装饰器接收的函数有所差别。
作用于方法
装饰器作用于类中的方法时,可以接收三个参数:
如果有返回值,则会作为目标方法的属性描述符。
示例:
function readonly(target: any, name: string, descriptor: PropertyDescriptor) {
console.log(target === Person.prototype); // 此例中输出:true
console.log(name); // 此例中输出:"sayHello"
console.log(descriptor);
/* 此例中输出:
{
value: [Function: sayHello],
writable: true,
enumerable: false,
configurable: true
}
*/
descriptor.writable = false;
}
class Person {
@readonly
sayHello() {
console.log("Hello Decorator");
}
}
console.log(Object.getOwnPropertyDescriptor(Person.prototype, "sayHello"));
/* 输出:
{
value: [Function: sayHello],
writable: false,
enumerable: false,
configurable: true
}
*/
const person = new Person();
// 以下两行代码执行任何一行都会报错,无法修改读取 person.sayHello 得到的值
Person.prototype.sayHello = function () {};
person.sayHello = function () {};
作用于访问器方法
装饰器作用于访问器方法时,接收参数与返回值作用和作用于方法时相同。其中,作用于实例访问器方法时,target
接收的是类的 [[prototype]]
,作用于静态访问器方法时接收的是类本身。
示例:
function limitAge(limit: number) {
return function (
target: any,
name: string,
descriptor: TypedPropertyDescriptor<number>
) {
console.log(target === Person.prototype); // 此例中输出:true
console.log(name); // 此例中输出:"age"
console.log(descriptor);
/* 此例中输出:
{
get: [Function: get age],
set: [Function: set age],
enumerable: false,
configurable: true
}
*/
const originalSetter = descriptor.set;
descriptor.set = function (age: number) {
if (age >= limit) return;
if (originalSetter) originalSetter.call(this, age);
};
};
}
class Person {
constructor(private _age: number) {}
get age() {
return this._age;
}
@limitAge(150)
set age(age: number) {
this._age = age;
}
}
const person = new Person(18);
person.age = 180;
console.log(person.age); // 输出:18
person.age = 81;
console.log(person.age); // 输出:81
作用于成员属性
装饰器作用于成员属性时,接收两个参数:
target
:作用于实例成员时是类的 [[prototype]]
,作用于静态成员时是类本身;
name
:属性名。
返回值会被忽略。
示例:
function checkProperty(target: any, name: string) {
let targetStr = "";
if (target === Person.prototype) {
targetStr = "类的 [[prototype]]";
} else if (target === Person) {
targetStr = "类本身";
} else return
console.log(`当前属性名为 ${name}, 装饰器接收的第一个参数是${targetStr}`);
}
class Person {
@checkProperty
static staticProperty = "vallue";
@checkProperty
instanceProperty: string = "value";
}
/* 最终控制台输出如下:
当前属性名为 instanceProperty, 装饰器接收的第一个参数是类的 [[prototype]]
当前属性名为 staticProperty, 装饰器接收的第一个参数是类本身
*/
作用于参数
装饰器作用于参数(可以是实例方法、静态方法或构造器中的参数,当然也包括访问器方法)时,接收三个参数:
target
:作用于实例成员时是类的 [[prototype]]
,作用于静态成员时是类本身;
name
:方法名,如果作用于 constructor
中的参数,则接收 undefined;
index
:目标参数在方法的参数列表中的索引。
返回值会被忽略。
示例:
function checkArg(target: any, name: string | undefined, index: number) {
let targetStr = "";
if (target === Test.prototype) {
targetStr = "类的 [[prototype]]";
} else if (target === Test) {
targetStr = "类本身";
} else return;
const funName = name ?? "constructor";
console.log(
`当前检测的是方法 ${funName} 中的第 ${
index + 1
} 个参数,装饰器接收的第一个参数是${targetStr}`
);
}
class Test {
static staticFun(arg1: any, @checkArg arg2: any) {}
constructor(@checkArg arg: any) {}
instanceFun(@checkArg arg1: any, arg2: any) {}
}
/* 最终控制台输出如下:
当前检测的是方法 instanceFun 中的第 1 个参数,装饰器接收的第一个参数是类的 [[prototype]]
当前检测的是方法 staticFun 中的第 2 个参数,装饰器接收的第一个参数是类本身
当前检测的是方法 constructor 中的第 1 个参数,装饰器接收的第一个参数是类本身
*/
同时也能看出,对实例方法、静态方法、构造器方法的参数分别使用装饰器时,装饰器执行的顺序是:实例 —> 静态 —> 构造器