zzc6332 发表于 2024-1-10 17:33

快速了解 TS 中装饰器的使用

## 简介

`TS` 中的装饰器(`Decorator`)是一种特殊的声明,用以对类进行装饰加工。它可以被附加到类的声明,以及类中的成员属性、方法、访问器、方法参数、构造器参数上。

`TS` 对装饰器提供了官方支持,它作为一个实验性功能,可以在 `tsconfig.json` 中将 `compilerOptions.experimentalDecorators` 设置为 `true` 开启。

装饰器的本质是函数,比如一个装饰器名为 `decorator`,则以 `@decorator` 的形式使用。装饰器对类的改变是在代码编译时发生的,编译后的代码中,定义被装饰的类时,装饰器函数会被调用,被装饰的内容相关的数据会作为参数传入装饰器函数中。

## 装饰器基本使用

以作用于类本身的装饰器为例:

- 接收一个参数 `target`,表示被装饰的类本身。
- 可以返回一个新的类以覆盖被装饰的类,如果不显式返回,则默认返回被装饰的类本身。

示例:

~~~typescript
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` 类:

~~~typescript
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
*/
~~~

可以看出,当对同一个目标使用多个装饰器时,如果使用的是装饰器工厂,则它们会按代码中的顺序从上到下执行得到装饰器,而装饰器本身会按从下到上的顺序执行。

## 作用于不同目标的装饰器

除了作用于类本身,也可以对类中的其它目标使用装饰器,对于不同的目标,有时装饰器接收的函数有所差别。

### 作用于方法

装饰器作用于类中的方法时,可以接收三个参数:

- `target`:作用于实例方法时是类的 `[]`,作用于静态方法时是类本身;
- `name`:方法名;
- `descriptor`:属性描述符,见 (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)。

如果有返回值,则会作为目标方法的属性描述符。

示例:

~~~typescript
function readonly(target: any, name: string, descriptor: PropertyDescriptor) {
console.log(target === Person.prototype); // 此例中输出:true
console.log(name); // 此例中输出:"sayHello"
console.log(descriptor);
/* 此例中输出:
    {
      value: ,
      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: ,
    writable: false,
    enumerable: false,
    configurable: true
}
*/

const person = new Person();

// 以下两行代码执行任何一行都会报错,无法修改读取 person.sayHello 得到的值
Person.prototype.sayHello = function () {};
person.sayHello = function () {};
~~~

### 作用于访问器方法

装饰器作用于访问器方法时,接收参数与返回值作用和作用于方法时相同。其中,作用于实例访问器方法时,`target` 接收的是类的 `[]`,作用于静态访问器方法时接收的是类本身。

示例:

~~~typescript
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: ,
      set: ,
      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`:作用于实例成员时是类的 `[]`,作用于静态成员时是类本身;
- `name`:属性名。

返回值会被忽略。

示例:

~~~typescript
function checkProperty(target: any, name: string) {
let targetStr = "";
if (target === Person.prototype) {
    targetStr = "类的 []";
} else if (target === Person) {
    targetStr = "类本身";
} else return
console.log(`当前属性名为 ${name}, 装饰器接收的第一个参数是${targetStr}`);
}

class Person {
@checkProperty
static staticProperty = "vallue";
@checkProperty
instanceProperty: string = "value";
}

/* 最终控制台输出如下:
    当前属性名为 instanceProperty, 装饰器接收的第一个参数是类的 []
    当前属性名为 staticProperty, 装饰器接收的第一个参数是类本身
*/
~~~

### 作用于参数

装饰器作用于参数(可以是实例方法、静态方法或构造器中的参数,当然也包括访问器方法)时,接收三个参数:

- `target`:作用于实例成员时是类的 `[]`,作用于静态成员时是类本身;
- `name`:方法名,如果作用于 `constructor` 中的参数,则接收 `undefined;`
- `index`:目标参数在方法的参数列表中的索引。

返回值会被忽略。

示例:

~~~typescript
function checkArg(target: any, name: string | undefined, index: number) {
let targetStr = "";
if (target === Test.prototype) {
    targetStr = "类的 []";
} 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 个参数,装饰器接收的第一个参数是类的 []
    当前检测的是方法 staticFun 中的第 2 个参数,装饰器接收的第一个参数是类本身
    当前检测的是方法 constructor 中的第 1 个参数,装饰器接收的第一个参数是类本身
*/
~~~

同时也能看出,对实例方法、静态方法、构造器方法的参数分别使用装饰器时,装饰器执行的顺序是:`实例 —> 静态 —> 构造器`

moruye 发表于 2024-1-10 21:33

三滑稽甲苯 发表于 2024-1-11 08:27

这个感觉和 Python 的装饰器很像
页: [1]
查看完整版本: 快速了解 TS 中装饰器的使用