TypeScript 装饰器
简介
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。在语法上,装饰器有如下几个特征。
- 第一个字符(或者说前缀)是
@
,后面是一个表达式。 @
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。- 这个函数接受所修饰对象的一些相关值作为参数。
- 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
下面就是一个最简单的装饰器,会在类执行前先执行装饰器,并向装饰器自动传入参数。
function simpleDecorator() {
console.log("hi");
}
@simpleDecorator
class A {} // "hi"
2
3
4
5
6
TS 从早期就支持装饰器。但是后来 ECMAScript 委员会通过的装饰器语法标准,与 TS 早期装饰器语法有很大的差异。
TS 5.0 之后同时支持两种装饰器语法。标准语法可以直接使用,传统语法需要打开 --experimentalDecorators
编译参数。本文介绍装饰器的标准写法。
装饰器结构
装饰器函数的类型定义如下:
type Decorator = (
value: DecoratedValue,
context: {
kind: string;
name: string | symbol;
addInitializer?(initializer: () => void): void;
static?: boolean;
private?: boolean;
access: {
get?(): unknown;
set?(value: unknown): void;
};
}
) => void | ReplacementValue;
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码中,Decorator
是装饰器的类型定义。它是一个函数,使用时会接收到 value
和 context
两个参数。
- value:所装饰的对象。
- context:上下文对象,TS 提供一个原生接口
ClassMethodDecoratorContext
,描述这个对象。
context
对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kind
和 name
)是必有的,其他都是可选的。
kind
:字符串,表示所装饰对象的类型,可能取以下的值:class
method
getter
setter
field
accessor
name
:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。addInitializer()
:函数,用来添加类的初始化逻辑。将初始化逻辑以函数参数的形式传入方法。注意,addInitializer()没有返回值。private
:布尔值,表示所装饰的对象是否为类的私有成员。static
:布尔值,表示所装饰的对象是否为类的静态成员。access
:一个对象,包含了某个值的 get 和 set 方法。
类装饰器
类装饰器的类型描述如下:
type ClassDecorator = (
value: Function,
context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}
) => Function | void;
2
3
4
5
6
7
8
类装饰器一般用来对类进行操作,可以不返回任何值:
function Greeter(value, context) {
if (context.kind === "class") {
value.prototype.greet = function () {
console.log("你好");
};
}
}
@Greeter
class User {}
let u = new User();
u.greet(); // "你好"
2
3
4
5
6
7
8
9
10
11
12
13
类装饰器可以返回一个函数,替代当前类的构造方法:
function countInstances(value: any, context: any) {
let instanceCount = 0;
const wrapper = function (...args: any[]) {
instanceCount++;
const instance = new value(...args);
instance.count = instanceCount;
return instance;
} as unknown as typeof MyClass;
wrapper.prototype = value.prototype; // 修改原型对象为原类的原型对象,否则instanceof无法通过
return wrapper;
}
@countInstances
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass; // true
inst1.count; // 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
类装饰器也可以返回一个新的类,替代原来所装饰的类:
function countInstances(value: any, context: any) {
let instanceCount = 0;
return class extends value {
constructor(...args: any[]) {
super(...args);
instanceCount++;
this.count = instanceCount;
}
};
}
@countInstances
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass; // true
inst1.count; // 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
下面的例子是通过类装饰器,禁止使用 new 命令新建类的实例:
function functionCallable(value: any, { kind }: any): any {
if (kind === "class") {
return function (...args: any) {
// new.target 属性允许你检测函数或构造方法是否是通过new运算符被调用的
if (new.target !== undefined) {
throw new TypeError("This function can’t be new-invoked");
}
return new value(...args);
};
}
}
@functionCallable
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
// @ts-ignore
const robin = Person("Robin");
robin.name; // 'Robin'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
类装饰器的上下文对象 context 的 addInitializer()方法,用来定义一个类的初始化函数,在类完全定义结束后执行:
function customElement(name: string) {
return <Input extends new (...args: any) => any>(
value: Input,
context: ClassDecoratorContext
) => {
context.addInitializer(function () {
// Window 对象上的一个只读属性,define 用于定义一个新的自定义元素
customElements.define(name, value);
});
};
}
@customElement("hello-world")
class MyComponent extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<h1>Hello World</h1>`;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
上面示例中,类 MyComponent
定义完成后,会自动执行类装饰器 @customElement()
给出的初始化函数,该函数会将当前类注册为指定名称(本例为<hello-world>
)的自定义 HTML 元素。
方法装饰器
方法装饰器用来装饰类的方法(method)。它的类型描述如下:
type ClassMethodDecorator = (
value: Function,
context: {
kind: "method";
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
2
3
4
5
6
7
8
9
10
11
方法装饰器会改写类的原始方法,,实质等同于下面的操作:
function trace(decoratedMethod) {
// ...
}
class C {
@trace
toString() {
return "C";
}
}
// `@trace` 等同于
// C.prototype.toString = trace(C.prototype.toString);
2
3
4
5
6
7
8
9
10
11
12
13
如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@log
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
function log(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`);
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`);
return result;
}
return replacementMethod;
}
const person = new Person("张三");
person.greet();
// "LOG: Entering method 'greet'."
// "Hello, my name is 张三."
// "LOG: Exiting method 'greet'."
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
下面再看一个例子,通过 addInitializer()将选定的方法名,放入一个集合:
function collect(value, { name, addInitializer }) {
addInitializer(function () {
if (!this.collectedMethodKeys) {
this.collectedMethodKeys = new Set();
}
this.collectedMethodKeys.add(name);
});
}
class C {
@collect
toString() {}
@collect
[Symbol.iterator]() {}
}
const inst = new C();
inst.collectedMethodKeys; // new Set(['toString', Symbol.iterator])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
属性装饰器
属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下:
type ClassFieldDecorator = (
value: undefined,
context: {
kind: "field";
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown; set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => (initialValue: unknown) => unknown | void;
2
3
4
5
6
7
8
9
10
11
装饰器的第一个参数 value 的类型是 undefined,这意味着这个参数实际上没用的,装饰器不能从 value 获取所装饰属性的值。
另外,第二个参数 context 对象的 kind 属性的值为字符串 field,而不是“property”或“attribute”
属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值:
function logged(value, context) {
const { kind, name } = context;
if (kind === "field") {
return function (initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return "red";
};
}
}
class Color {
@logged name = "green";
}
const color = new Color();
// "initializing name with value green"
console.log(color.name); // red
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
属性装饰器的上下文对象 context
的 access
属性,提供所装饰属性的存取器,请看下面的例子:
let acc;
function exposeAccess(value, { access }) {
acc = access;
}
class Color {
@exposeAccess
name = "green";
}
const green = new Color();
green.name; // 'green'
acc.get(green); // 'green'
acc.set(green, "red");
green.name; // 'red'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getter、setter 装饰器
类的取值器(getter)和存值器(setter)的装饰器,它们的类型描述如下:
type ClassGetterDecorator = (
value: Function,
context: {
kind: "getter";
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
type ClassSetterDecorator = (
value: Function,
context: {
kind: "setter";
name: string | symbol;
static: boolean;
private: boolean;
access: { set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => Function | void;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
getter 装饰器的上下文对象 context 的
access
属性,只包含get()
方法;setter 装饰器的access
属性,只包含set()
方法
这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器:
class C {
@lazy
get value() {
console.log("正在计算……");
return "开销大的计算结果";
}
}
function lazy(value: any, { kind, name }: any) {
if (kind === "getter") {
return function (this: any) {
const result = value.call(this);
Object.defineProperty(this, name, {
value: result,
writable: false,
});
return result;
};
}
return;
}
const inst = new C();
inst.value;
// 正在计算……
// '开销大的计算结果'
inst.value;
// '开销大的计算结果'
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
accessor 装饰器
装饰器语法引入了一个新的属性修饰符 accessor
,可以与静态属性和私有属性一起使用:
class C {
static accessor x = 1;
accessor #y = 2;
}
2
3
4
上面示例中,accessor
修饰符等同于为公开属性 x
自动生成取值器和存值器,它们作用于私有属性 x
。(注意,公开的 x 与私有的 x 不是同一个属性)
accessor 装饰器的类型如下:
type ClassAutoAccessorDecorator = (
value: {
get: () => unknown;
set: (value: unknown) => void;
},
context: {
kind: "accessor";
name: string | symbol;
access: { get(): unknown; set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}
) => {
get?: () => unknown;
set?: (value: unknown) => void;
init?: (initialValue: unknown) => unknown;
} | void;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
accessor
装饰器的 value
参数,是一个包含 get()
方法和 set()
方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的 get()
方法和 set()
方法。此外,装饰器返回的对象还可以包括一个 init()
方法,用来改变私有属性的初始值:
class C {
@logged accessor x = 1;
}
function logged(value, { kind, name }) {
if (kind === "accessor") {
let { get, set } = value;
return {
get() {
console.log(`getting ${name}`);
return get.call(this);
},
set(val) {
console.log(`setting ${name} to ${val}`);
return set.call(this, val);
},
init(initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return initialValue;
},
};
}
}
let c = new C();
c.x;
// getting x
c.x = 123;
// setting x to 123
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
装饰器的执行顺序
装饰器的执行分为两个阶段。
- 评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。
- 应用(application):将评估装饰器后得到的函数,应用于所装饰对象。
应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器:
function d(str: string) {
console.log(`评估 @d(): ${str}`);
return (value: any, context: any) => console.log(`应用 @d(): ${str}`);
}
function log(str: string) {
console.log(str);
return str;
}
@d("类装饰器")
class T {
@d("静态属性装饰器")
static staticField = log("静态属性值");
@d("原型方法")
[log("计算方法名")]() {}
@d("实例属性")
instanceField = log("实例属性值");
@d("静态方法装饰器")
static fn() {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面的运行结果如下:
评估 @d(): 类装饰器
评估 @d(): 静态属性装饰器
评估 @d(): 原型方法
计算方法名
评估 @d(): 实例属性
评估 @d(): 静态方法装饰器
应用 @d(): 静态方法装饰器
应用 @d(): 原型方法
应用 @d(): 静态属性装饰器
应用 @d(): 实例属性
应用 @d(): 类装饰器
静态属性值
2
3
4
5
6
7
8
9
10
11
12
可以看到,类载入的时候,代码按照以下顺序执行:
- 装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。
注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。
- 装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。
静态方法装饰器首先应用,然后是原型方法的装饰器和静态属性装饰器,接下来是实例属性装饰器,最后是类装饰器。
注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。
如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound // 针对log得到的结果再执行
@log // 先执行
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
2
3
4
5
6
7
8
9
10
11
12