TypeScript 装饰器

简介

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。

在语法上,装饰器有如下几个特征。

(1)第一个字符(或者说前缀)是@,后面是一个表达式。

(2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。

(3)这个函数接受所修饰对象的一些相关值作为参数。

(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

举例来说,有一个函数Injectable()当作装饰器使用,那么需要写成@Injectable,然后放在某个类的前面。

  1. @Injectable class A {
  2. // ...
  3. }

上面示例中,由于有了装饰器@Injectable,类A的行为在运行时就会发生改变。

下面就是一个最简单的装饰器。

  1. function simpleDecorator() {
  2. console.log('hi');
  3. }
  4. @simpleDecorator
  5. class A {} // "hi"

上面示例中,函数simpleDecorator()用作装饰器,附加在类A之上,后者在代码解析时就会打印一行日志。

编译上面的代码会报错,提示没有用到装饰器的参数。现在就为装饰器加上参数,让它更像正式运行的代码。

  1. function simpleDecorator(
  2. target:any,
  3. context:any
  4. ) {
  5. console.log('hi, this is ' + target);
  6. return target;
  7. }
  8. @simpleDecorator
  9. class A {} // "hi, this is class A {}"

上面的代码就可以顺利通过编译了,代码含义这里先不解释。大家只要理解,类A在执行前会先执行装饰器simpleDecorator(),并且会向装饰器自动传入参数就可以了。

装饰器有多种形式,基本上只要在@符号后面添加表达式都是可以的。下面都是合法的装饰器。

  1. @myFunc
  2. @myFuncFactory(arg1, arg2)
  3. @libraryModule.prop
  4. @someObj.method(123)
  5. @(wrap(dict['prop']))

注意,@后面的表达式,最终执行后得到的应该是一个函数。

相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。

  1. @frozen class Foo {
  2. @configurable(false)
  3. @enumerable(true)
  4. method() {}
  5. @throttle(500)
  6. expensiveMethod() {}
  7. }

上面示例中,一共有四个装饰器,一个用在类本身(@frozen),另外三个用在类的方法(@configurable@enumerable@throttle)。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。

装饰器的版本

TypeScript 从早期开始,就支持装饰器。但是,装饰器的语法后来发生了变化。ECMAScript 标准委员会最终通过的语法标准,与 TypeScript 早期使用的语法有很大差异。

目前,TypeScript 5.0 同时支持两种装饰器语法。标准语法可以直接使用,传统语法需要打开--experimentalDecorators编译参数。

  1. $ tsc --target ES5 --experimentalDecorators

本章介绍装饰器的标准语法,下一章介绍传统语法。

装饰器的结构

装饰器函数的类型定义如下。

  1. type Decorator = (
  2. value: DecoratedValue,
  3. context: {
  4. kind: string;
  5. name: string | symbol;
  6. addInitializer?(initializer: () => void): void;
  7. static?: boolean;
  8. private?: boolean;
  9. access: {
  10. get?(): unknown;
  11. set?(value: unknown): void;
  12. };
  13. }
  14. ) => void | ReplacementValue;

上面代码中,Decorator是装饰器的类型定义。它是一个函数,使用时会接收到valuecontext两个参数。

  • value:所装饰的对象。
  • context:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext,描述这个对象。
  1. function decorator(
  2. value:any,
  3. context:ClassMethodDecoratorContext
  4. ) {
  5. // ...
  6. }

上面是一个装饰器函数,其中第二个参数context的类型就可以写成ClassMethodDecoratorContext

context对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kindname)是必有的,其他都是可选的。

(1)kind:字符串,表示所装饰对象的类型,可能取以下的值。

  • ‘class’
  • ‘method’
  • ‘getter’
  • ‘setter’
  • ‘field’
  • ‘accessor’

这表示一共有六种类型的装饰器。

(2)name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。

(3)addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值。

(4)private:布尔值,表示所装饰的对象是否为类的私有成员。

(5)static:布尔值,表示所装饰的对象是否为类的静态成员。

(6)access:一个对象,包含了某个值的 get 和 set 方法。

类装饰器

类装饰器的类型描述如下。

  1. type ClassDecorator = (
  2. value: Function,
  3. context: {
  4. kind: 'class';
  5. name: string | undefined;
  6. addInitializer(initializer: () => void): void;
  7. }
  8. ) => Function | void;

类装饰器接受两个参数:value(当前类本身)和context(上下文对象)。其中,context对象的kind属性固定为字符串class

类装饰器一般用来对类进行操作,可以不返回任何值,请看下面的例子。

  1. function Greeter(value, context) {
  2. if (context.kind === 'class') {
  3. value.prototype.greet = function () {
  4. console.log('你好');
  5. };
  6. }
  7. }
  8. @Greeter
  9. class User {}
  10. let u = new User();
  11. u.greet(); // "你好"

上面示例中,类装饰器@Greeter在类User的原型对象上,添加了一个greet()方法,实例就可以直接使用该方法。

类装饰器可以返回一个函数,替代当前类的构造方法。

  1. function countInstances(value:any, context:any) {
  2. let instanceCount = 0;
  3. const wrapper = function (...args:any[]) {
  4. instanceCount++;
  5. const instance = new value(...args);
  6. instance.count = instanceCount;
  7. return instance;
  8. } as unknown as typeof MyClass;
  9. wrapper.prototype = value.prototype; // A
  10. return wrapper;
  11. }
  12. @countInstances
  13. class MyClass {}
  14. const inst1 = new MyClass();
  15. inst1 instanceof MyClass // true
  16. inst1.count // 1

上面示例中,类装饰器@countInstances返回一个函数,替换了类MyClass的构造方法。新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且对实例添加count属性,表示当前实例的编号。

注意,上例为了确保新构造方法继承定义在MyClass的原型之上的成员,特别加入A行,确保两者的原型对象是一致的。否则,新的构造函数wrapper的原型对象,与MyClass不同,通不过instanceof运算符。

类装饰器也可以返回一个新的类,替代原来所装饰的类。

  1. function countInstances(value:any, context:any) {
  2. let instanceCount = 0;
  3. return class extends value {
  4. constructor(...args:any[]) {
  5. super(...args);
  6. instanceCount++;
  7. this.count = instanceCount;
  8. }
  9. };
  10. }
  11. @countInstances
  12. class MyClass {}
  13. const inst1 = new MyClass();
  14. inst1 instanceof MyClass // true
  15. inst1.count // 1

上面示例中,@countInstances返回一个MyClass的子类。

下面的例子是通过类装饰器,禁止使用new命令新建类的实例。

  1. function functionCallable(
  2. value as any, {kind} as any
  3. ) {
  4. if (kind === 'class') {
  5. return function (...args) {
  6. if (new.target !== undefined) {
  7. throw new TypeError('This function can’t be new-invoked');
  8. }
  9. return new value(...args);
  10. }
  11. }
  12. }
  13. @functionCallable
  14. class Person {
  15. constructor(name) {
  16. this.name = name;
  17. }
  18. }
  19. const robin = Person('Robin');
  20. robin.name // 'Robin'

上面示例中,类装饰器@functionCallable返回一个新的构造方法,里面判断new.target是否不为空,如果是的,就表示通过new命令调用,从而报错。

类装饰器的上下文对象contextaddInitializer()方法,用来定义一个类的初始化函数,在类完全定义结束后执行。

  1. function customElement(name: string) {
  2. return <Input extends new (...args: any) => any>(
  3. value: Input,
  4. context: ClassDecoratorContext
  5. ) => {
  6. context.addInitializer(function () {
  7. customElements.define(name, value);
  8. });
  9. };
  10. }
  11. @customElement("hello-world")
  12. class MyComponent extends HTMLElement {
  13. constructor() {
  14. super();
  15. }
  16. connectedCallback() {
  17. this.innerHTML = `<h1>Hello World</h1>`;
  18. }
  19. }

上面示例中,类MyComponent定义完成后,会自动执行类装饰器@customElement()给出的初始化函数,该函数会将当前类注册为指定名称(本例为<hello-world>)的自定义 HTML 元素。

方法装饰器

方法装饰器用来装饰类的方法(method)。它的类型描述如下。

  1. type ClassMethodDecorator = (
  2. value: Function,
  3. context: {
  4. kind: 'method';
  5. name: string | symbol;
  6. static: boolean;
  7. private: boolean;
  8. access: { get: () => unknown };
  9. addInitializer(initializer: () => void): void;
  10. }
  11. ) => Function | void;

根据上面的类型,方法装饰器是一个函数,接受两个参数:valuecontext

参数value是方法本身,参数context是上下文对象,有以下属性。

  • kind:值固定为字符串method,表示当前为方法装饰器。
  • name:所装饰的方法名,类型为字符串或 Symbol 值。
  • static:布尔值,表示是否为静态方法。该属性为只读属性。
  • private:布尔值,表示是否为私有方法。该属性为只读属性。
  • access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。
  • addInitializer():为方法增加初始化函数。

方法装饰器会改写类的原始方法,实质等同于下面的操作。

  1. function trace(decoratedMethod) {
  2. // ...
  3. }
  4. class C {
  5. @trace
  6. toString() {
  7. return 'C';
  8. }
  9. }
  10. // `@trace` 等同于
  11. // C.prototype.toString = trace(C.prototype.toString);

上面示例中,@trace是方法toString()的装饰器,它的效果等同于最后一行对toString()的改写。

如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。

  1. function replaceMethod() {
  2. return function () {
  3. return `How are you, ${this.name}?`;
  4. }
  5. }
  6. class Person {
  7. constructor(name) {
  8. this.name = name;
  9. }
  10. @replaceMethod
  11. hello() {
  12. return `Hi ${this.name}!`;
  13. }
  14. }
  15. const robin = new Person('Robin');
  16. robin.hello() // 'How are you, Robin?'

上面示例中,装饰器@replaceMethod返回的函数,就成为了新的hello()方法。

下面是另一个例子。

  1. class Person {
  2. name: string;
  3. constructor(name: string) {
  4. this.name = name;
  5. }
  6. @log
  7. greet() {
  8. console.log(`Hello, my name is ${this.name}.`);
  9. }
  10. }
  11. function log(originalMethod:any, context:ClassMethodDecoratorContext) {
  12. const methodName = String(context.name);
  13. function replacementMethod(this: any, ...args: any[]) {
  14. console.log(`LOG: Entering method '${methodName}'.`)
  15. const result = originalMethod.call(this, ...args);
  16. console.log(`LOG: Exiting method '${methodName}'.`)
  17. return result;
  18. }
  19. return replacementMethod;
  20. }
  21. const person = new Person('张三');
  22. person.greet()
  23. // "LOG: Entering method 'greet'."
  24. // "Hello, my name is 张三."
  25. // "LOG: Exiting method 'greet'."

上面示例中,装饰器@log的返回值是一个函数replacementMethod,替代了原始方法greet()。在replacementMethod()内部,通过执行originalMethod.call()完成了对原始方法的调用。

利用方法装饰器,可以将类的方法变成延迟执行。

  1. function delay(milliseconds: number = 0) {
  2. return function (value, context) {
  3. if (context.kind === "method") {
  4. return function (...args: any[]) {
  5. setTimeout(() => {
  6. value.apply(this, args);
  7. }, milliseconds);
  8. };
  9. }
  10. };
  11. }
  12. class Logger {
  13. @delay(1000)
  14. log(msg: string) {
  15. console.log(`${msg}`);
  16. }
  17. }
  18. let logger = new Logger();
  19. logger.log("Hello World");

上面示例中,方法装饰器@delay(1000)将方法log()的执行推迟了1秒(1000毫秒)。这里真正的方法装饰器,是delay()执行后返回的函数,delay()的作用是接收参数,用来设置推迟执行的时间。这种通过高阶函数返回装饰器的做法,称为“工厂模式”,即可以像工厂那样生产出一个模子的装饰器。

方法装饰器的参数context对象里面,有一个addInitializer()方法。它是一个钩子方法,用来在类的初始化阶段,添加回调函数。这个回调函数就是作为addInitializer()的参数传入的,它会在构造方法执行期间执行,早于属性(field)的初始化。

下面是addInitializer()方法的一个例子。我们知道,类的方法往往需要在构造方法里面,进行this的绑定。

  1. class Person {
  2. name: string;
  3. constructor(name: string) {
  4. this.name = name;
  5. // greet() 绑定 this
  6. this.greet = this.greet.bind(this);
  7. }
  8. greet() {
  9. console.log(`Hello, my name is ${this.name}.`);
  10. }
  11. }
  12. const g = new Person('张三').greet;
  13. g() // "Hello, my name is 张三."

上面例子中,类Person的构造方法内部,将thisgreet()方法进行了绑定。如果没有这一行,将greet()赋值给变量g进行调用,就会报错了。

this的绑定必须放在构造方法里面,因为这必须在类的初始化阶段完成。现在,它可以移到方法装饰器的addInitializer()里面。

  1. function bound(
  2. originalMethod:any, context:ClassMethodDecoratorContext
  3. ) {
  4. const methodName = context.name;
  5. if (context.private) {
  6. throw new Error(`不能绑定私有方法 ${methodName as string}`);
  7. }
  8. context.addInitializer(function () {
  9. this[methodName] = this[methodName].bind(this);
  10. });
  11. }

上面示例中,绑定this转移到了addInitializer()方法里面。

下面再看一个例子,通过addInitializer()将选定的方法名,放入一个集合。

  1. function collect(
  2. value,
  3. {name, addInitializer}
  4. ) {
  5. addInitializer(function () {
  6. if (!this.collectedMethodKeys) {
  7. this.collectedMethodKeys = new Set();
  8. }
  9. this.collectedMethodKeys.add(name);
  10. });
  11. }
  12. class C {
  13. @collect
  14. toString() {}
  15. @collect
  16. [Symbol.iterator]() {}
  17. }
  18. const inst = new C();
  19. inst.@collect // new Set(['toString', Symbol.iterator])

上面示例中,方法装饰器@collect会将所装饰的成员名字,加入一个 Set 集合collectedMethodKeys

属性装饰器

属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。

  1. type ClassFieldDecorator = (
  2. value: undefined,
  3. context: {
  4. kind: 'field';
  5. name: string | symbol;
  6. static: boolean;
  7. private: boolean;
  8. access: { get: () => unknown, set: (value: unknown) => void };
  9. addInitializer(initializer: () => void): void;
  10. }
  11. ) => (initialValue: unknown) => unknown | void;

注意,装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field,而不是“property”或“attribute”,这一点是需要注意的。

属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。

  1. function logged(value, context) {
  2. const { kind, name } = context;
  3. if (kind === 'field') {
  4. return function (initialValue) {
  5. console.log(`initializing ${name} with value ${initialValue}`);
  6. return initialValue;
  7. };
  8. }
  9. }
  10. class Color {
  11. @logged name = 'green';
  12. }
  13. const color = new Color();
  14. // "initializing name with value green"

上面示例中,属性装饰器@logged装饰属性name@logged的返回值是一个函数,该函数用来对属性name进行初始化,它的参数initialValue就是属性name的初始值green。新建实例对象color时,该函数会自动执行。

属性装饰器的返回值函数,可以用来更改属性的初始值。

  1. function twice() {
  2. return initialValue => initialValue * 2;
  3. }
  4. class C {
  5. @twice
  6. field = 3;
  7. }
  8. const inst = new C();
  9. inst.field // 6

上面示例中,属性装饰器@twice返回一个函数,该函数的返回值是属性field的初始值乘以2,所以属性field的最终值是6。

属性装饰器的上下文对象contextaccess属性,提供所装饰属性的存取器,请看下面的例子。

  1. let acc;
  2. function exposeAccess(
  3. value, {access}
  4. ) {
  5. acc = access;
  6. }
  7. class Color {
  8. @exposeAccess
  9. name = 'green'
  10. }
  11. const green = new Color();
  12. green.name // 'green'
  13. acc.get(green) // 'green'
  14. acc.set(green, 'red');
  15. green.name // 'red'

上面示例中,access包含了属性name的存取器,可以对该属性进行取值和赋值。

getter 装饰器,setter 装饰器

getter 装饰器和 setter 装饰器,是分别针对类的取值器(getter)和存值器(setter)的装饰器。它们的类型描述如下。

  1. type ClassGetterDecorator = (
  2. value: Function,
  3. context: {
  4. kind: 'getter';
  5. name: string | symbol;
  6. static: boolean;
  7. private: boolean;
  8. access: { get: () => unknown };
  9. addInitializer(initializer: () => void): void;
  10. }
  11. ) => Function | void;
  12. type ClassSetterDecorator = (
  13. value: Function,
  14. context: {
  15. kind: 'setter';
  16. name: string | symbol;
  17. static: boolean;
  18. private: boolean;
  19. access: { set: (value: unknown) => void };
  20. addInitializer(initializer: () => void): void;
  21. }
  22. ) => Function | void;

注意,getter 装饰器的上下文对象contextaccess属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。

这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。

下面的例子是将取值器的结果,保存为一个属性,加快后面的读取。

  1. class C {
  2. @lazy
  3. get value() {
  4. console.log('正在计算……');
  5. return '开销大的计算结果';
  6. }
  7. }
  8. function lazy(
  9. value:any,
  10. {kind, name}:any
  11. ) {
  12. if (kind === 'getter') {
  13. return function (this:any) {
  14. const result = value.call(this);
  15. Object.defineProperty(
  16. this, name,
  17. {
  18. value: result,
  19. writable: false,
  20. }
  21. );
  22. return result;
  23. };
  24. }
  25. return;
  26. }
  27. const inst = new C();
  28. inst.value
  29. // 正在计算……
  30. // '开销大的计算结果'
  31. inst.value
  32. // '开销大的计算结果'

上面示例中,第一次读取inst.value,会进行计算,然后装饰器@lazy将结果存入只读属性value,后面再读取这个属性,就不会进行计算了。

accessor 装饰器

装饰器语法引入了一个新的属性修饰符accessor

  1. class C {
  2. accessor x = 1;
  3. }

上面示例中,accessor修饰符等同于为属性x自动生成取值器和存值器,它们作用于私有属性x。也就是说,上面的代码等同于下面的代码。

  1. class C {
  2. #x = 1;
  3. get x() {
  4. return this.#x;
  5. }
  6. set x(val) {
  7. this.#x = val;
  8. }
  9. }

accessor也可以与静态属性和私有属性一起使用。

  1. class C {
  2. static accessor x = 1;
  3. accessor #y = 2;
  4. }

accessor 装饰器的类型如下。

  1. type ClassAutoAccessorDecorator = (
  2. value: {
  3. get: () => unknown;
  4. set(value: unknown) => void;
  5. },
  6. context: {
  7. kind: "accessor";
  8. name: string | symbol;
  9. access: { get(): unknown, set(value: unknown): void };
  10. static: boolean;
  11. private: boolean;
  12. addInitializer(initializer: () => void): void;
  13. }
  14. ) => {
  15. get?: () => unknown;
  16. set?: (value: unknown) => void;
  17. init?: (initialValue: unknown) => unknown;
  18. } | void;

accessor 装饰器的value参数,是一个包含get()方法和set()方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()方法和set()方法。此外,装饰器返回的对象还可以包括一个init()方法,用来改变私有属性的初始值。

下面是一个例子。

  1. class C {
  2. @logged accessor x = 1;
  3. }
  4. function logged(value, { kind, name }) {
  5. if (kind === "accessor") {
  6. let { get, set } = value;
  7. return {
  8. get() {
  9. console.log(`getting ${name}`);
  10. return get.call(this);
  11. },
  12. set(val) {
  13. console.log(`setting ${name} to ${val}`);
  14. return set.call(this, val);
  15. },
  16. init(initialValue) {
  17. console.log(`initializing ${name} with value ${initialValue}`);
  18. return initialValue;
  19. }
  20. };
  21. }
  22. }
  23. let c = new C();
  24. c.x;
  25. // getting x
  26. c.x = 123;
  27. // setting x to 123

上面示例中,装饰器@logged为属性x的存值器和取值器,加上了日志输出。

装饰器的执行顺序

装饰器的执行分为两个阶段。

(1)评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。

(2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。

也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。

应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。

请看下面的例子。

  1. function d(str:string) {
  2. console.log(`评估 @d(): ${str}`);
  3. return (
  4. value:any, context:any
  5. ) => console.log(`应用 @d(): ${str}`);
  6. }
  7. function log(str:string) {
  8. console.log(str);
  9. return str;
  10. }
  11. @d('类装饰器')
  12. class T {
  13. @d('静态属性装饰器')
  14. static staticField = log('静态属性值');
  15. @d('原型方法')
  16. [log('计算方法名')]() {}
  17. @d('实例属性')
  18. instanceField = log('实例属性值');
  19. }

上面示例中,类T有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。

它的运行结果如下。

  1. // "评估 @d(): 类装饰器"
  2. // "评估 @d(): 静态属性装饰器"
  3. // "评估 @d(): 原型方法"
  4. // "计算方法名"
  5. // "评估 @d(): 实例属性"
  6. // "应用 @d(): 原型方法"
  7. // "应用 @d(): 静态属性装饰器"
  8. // "应用 @d(): 实例属性"
  9. // "应用 @d(): 类装饰器"
  10. // "静态属性值"

可以看到,类载入的时候,代码按照以下顺序执行。

(1)装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。

注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。

(2)装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。

原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。

注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。

如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。

  1. class Person {
  2. name: string;
  3. constructor(name: string) {
  4. this.name = name;
  5. }
  6. @bound
  7. @log
  8. greet() {
  9. console.log(`Hello, my name is ${this.name}.`);
  10. }
  11. }

上面示例中,greet()有两个装饰器,内层的@log先执行,外层的@bound针对得到的结果再执行。

参考链接