装饰器现在其实已经广泛应用于前端领域,以及一些框架和开发工具中。例如:在 React 开发中,Redux 使用装饰器来连接组件和状态;在 NestJS 框架中,装饰器被用于实现依赖注入和中间件。这些都是利用装饰器实现的,使我们的代码更加简洁,逻辑更加清晰。
随着 TypeScript 5.0 版本正式支持 Stage3 的 Decorators(装饰器) - 提案,装饰器这个特性可以被我们更加广泛的应用。且进入 Stage3 阶段的提案往往也代表着不会有大的变更,本文也将基于这个版本的装饰器使用进行入门的探索。
装饰器的历史
装饰器的概念源于 Python 和其他使用类似结构的编程语言,主要用于修改或扩展类、方法和属性的行为。
最初的 JavaScript 装饰器提案于 2014 年提出,自那时以来,已经开发出多个版本的提案,目前的版本正在 ECMAScript 标准化过程的第 3 阶段。
过往:实验性装饰器
实际上,TypeScript 在 5.0 版本之前就已经支持了实验性的装饰器。但是,由于它是基于较早的提案来实现的,因此与最新的提案存在一些差异:
- 需要在 tsconfig 中开启
experimentalDecorators - decorator 方法与 Stage3 中的参数有差异
- 元数据需要通过
reflect-metadata来获取,如类型等
本文省略这部分内容,感兴趣的可以查看这个文档 https://www.typescriptlang.org/docs/handbook/decorators.html
现在:Stage3 装饰器
现在基于 Stage3 提案的 Decorators 在默认配置下就可以使用了,如果 tsconfig 中指定了 experimentalDecorators 需要移除,否则还是会 fallback 到之前的实验性装饰器上。
新的装饰器 Decorator 的方法接受两个参数,类型定义如下:
type Decorator<T> = (
value: T, // 对应的字段值
context: DecoratorContext,
) => void | T;
以上的装饰器类型又分为以下几种:
Class、Method、Getter、Setter、Field、Accessor
type ClassMemberDecoratorContext =
| ClassMethodDecoratorContext
| ClassGetterDecoratorContext
| ClassSetterDecoratorContext
| ClassFieldDecoratorContext
| ClassAccessorDecoratorContext;
type DecoratorContext =
| ClassDecoratorContext
| ClassMemberDecoratorContext;
Class
类型定义
展开查看
interface ClassDecoratorContext<
Class extends abstract new (...args: any) => any = abstract new (...args: any) => any,
> {
readonly kind: "class";
readonly name: string | undefined;
addInitializer(initializer: (this: Class) => void): void;
readonly metadata: DecoratorMetadata;
}
实例
收集初始化的实例:
class InstanceCollector {
instances = new Set();
install = <Class extends Constructor>(
value: Class,
context: ClassDecoratorContext,
) => {
const _this = this;
return class extends value {
constructor(...args: any[]) {
console.log(
"class constructor",
context.name,
context.metadata,
context.kind,
);
super(...args);
_this.instances.add(this);
}
};
};
}
const collector = new InstanceCollector();
@collector.install
class MyClass {
name: string;
constructor(name: string) {
this.name = name;
}
}
const inst1 = new MyClass("1");
const inst2 = new MyClass("2");
const inst3 = new MyClass("3");
console.log("instances: ", collector.instances);
/** Console
instances: Set(3) {
MyClass { name: '1' },
MyClass { name: '2' },
MyClass { name: '3' }
}
**/
Method
实际应用
个人认为最实用的一个装饰器,目前想到一些比较实用的用法如下:
- 对函数的入参做合法性校验
- 对于函数的调用打 log
- try catch 做容错处理
示例
下面这个示例是对函数调用打 log 和 try catch 处理:
function log(originalMethod: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
const className = String(this.constructor.name);
console.log(`LOG: Entering method '${className}.${methodName}'.`);
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${className}.${methodName}'.`);
return result;
}
return replacementMethod;
}
function catchErr(
originalMethod: Function,
context: ClassMethodDecoratorContext,
) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
const className = String(this.constructor.name);
try {
const result = originalMethod.call(this, ...args);
return result;
} catch (e) {
console.error(`Catch A Error in method '${className}.${methodName}'.`, e);
return null;
}
};
}
class BankService {
@log
callAPI() {
console.log("call api");
}
// 组合装饰器
@log
@catchErr
callAPIWithError() {
throw new Error("error");
}
}
const service = new BankService();
service.callAPI();
service.callAPIWithError();
console.log("----exit-----");
Field
示例
function fieldD(val: any, context: ClassFieldDecoratorContext) {
return function (initVal: any) {
console.log(initVal, context.name);
return initVal + 1;
};
}
class A {
@fieldD
name: string = "name";
@fieldD
age: number = 18;
@fieldD
static staticName: string = "staticName";
}
const a = new A();
console.log(a);
/** Console
staticName staticName
name name
18 age
A { name: 'name1', age: 19 }
**/
Accessor
类装饰器引入了一个新命令 accessor。
class C {
accessor x = 1;
}
它其实是一种语法糖,相当于声明属性 x 是私有属性 #x 的存取接口。上面的代码等同于下面的代码:
class C {
#x = 1;
get x() {
return this.#x;
}
set x(val) {
this.#x = val;
}
}
示例
function readonly<This, Return = number>(
target: ClassAccessorDecoratorTarget<This, Return>,
context: ClassAccessorDecoratorContext<This, Return>,
) {
const result: ClassAccessorDecoratorResult<This, Return> = {
get(this: This) {
return target.get.call(this);
},
set(value: Return) {
throw new Error(
`Cannot assign to read-only property '${String(context.name)}'.`,
);
},
init(this: This, value: Return) {
console.log("init value", value);
return value;
},
};
return result;
}
class C {
@readonly
accessor x: number = 1;
}
const c = new C();
console.log(c.x);
c.x = 10; // Error: Cannot assign to read-only property 'x'.
console.log(c.x);
Getter/Setter
示例
以下示例中 getter/setter 都将原始值 * 2,并打印出相关过程:
function doubleGetter(value: any, context: ClassGetterDecoratorContext) {
return function (this: any) {
const result = value.call(this) * 2;
console.log(
"[doubleGetter]",
"originalValue:",
value.call(this),
"doubleValue:",
result,
);
return result;
};
}
function doubleSetter(value: any, context: ClassSetterDecoratorContext) {
return function (this: any, val: number) {
const doubleValue = val * 2;
console.log(
"[doubleSetter]",
"originalValue:",
val,
"doubleValue:",
doubleValue,
);
return value.call(this, doubleValue);
};
}
class Counter {
_num: number;
constructor() {
this._num = 0;
}
@doubleGetter
get num() {
return this._num;
}
@doubleSetter
set num(val: number) {
this._num = val;
}
}
const counter = new Counter();
counter.num = 1;
console.log(counter.num);
/** Console
[doubleSetter] originalValue: 1 doubleValue: 2
[doubleGetter] originalValue: 2 doubleValue: 4
4
**/
以上的 demo 代码放在 github 上: https://github.com/hongzzz/typescript-decorators-demo