装饰器
之前项目里用TS的装饰器比较多,但是感觉现在大多数前端开发用装饰器都是用的TS装饰器,很少见用JS装饰器,特别是stage3的。也很少见什么文章会写到它们的区别,小鱼就在这简单介绍对比一下。
TS装饰器
PS: 若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项。
装饰器以@expression的形式加到类、类方法、类属性和访问符上,它会在运行时被调用,被装饰的声明信息做为参数传入。
多个装饰器时:由上至下依次对装饰器表达式求值;求值的结果会被当作函数,由下至上依次调用。简单理解就是从内到外调用装饰器。
类装饰器
为类扩展功能。
参数: constructor 类的构造函数;
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。(PS 需要自己处理好原来的原型链。)
例 密封类的构造函数和原型
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
方法装饰器
可以用来监视,修改或者替换方法定义。(项目里用的比较多的)
参数:
- target: any 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;
- propertyKey: string 成员的名字;
- descriptor: PropertyDescriptor 成员的属性描述符;
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
方法的属性描述符是一个对象,包含了被装饰的属性的一些信息。
- value: 被装饰属性的值,如果是方法,就是方法的引用。
- writable: 表示属性的值是否可以被修改,默认为 true。
- enumerable: 表示属性是否可以被枚举,默认为 false。
- configurable: 表示属性的描述符是否可以被修改或属性是否可以被删除,默认为 false。
在装饰器中,可以通过修改 descriptor 对象的这些属性来改变被装饰属性的行为。
项目例子
项目里的实际例子,followUp 在被装饰的函数执行之后,以其返回值为入参自动执行followup传入的函数,followUp可以将函数以字符串或函数的形式传入。在和后端对接口时很实用。
使用代码:
// store.ts
class TestStore {
b = (res: unknown) => console.log(res);
@followUp('b')
a() {
console.log('function a');
return 'a done';
}
}
const testStore = new TestStore();
testStore.a(); // function a
// a done
followUp的使用场景,例如表单展示页的筛选项,在设置筛选条件的方法执行后需要重新拉取表单,就可以用到followUp。再例如更新数据,update函数执行后也需要重新拉取表单,也可以使用该装饰器。
装饰器代码:
export const followUp = <T>(func: T[keyof T] | keyof T) => (
target: T,
property: string,
descriptor: TypedPropertyDescriptor<T>
) => {
// 先判断装饰的是不是函数
const original = descriptor.value;
if (typeof original !== 'function') {
throw new Error('@followUp must decorator a function');
}
// 修改装饰的方法,使其添加别的功能
descriptor.value = function (...args: any) {
// 执行了装饰的方法
const promise = original.call(this, ...args);
// 定义函数,能自定义参数(也就是被装饰的函数返回的结果)作为入参执行装饰器传入的函数
const followHandler = (d: any) => {
return this[func](d);
throw new Error(`func must be a function or string, but currently it is ${func}`);
};
// 这里判断返回值是否为Promise类型,如果是则在then中取实际值执行followHandler函数。
if (!(promise instanceof Promise)) {
return followHandler(promise);
}
return (promise as Promise<any>).then((d) => {
const res = followHandler(d);
return res;
});
};
};
属性装饰器
参数:
- target: any 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;
- propertyKey: string 属性名称;
- 没有属性描述符;
例 初始化属性
const initCarPropertyDec = <T>(property: T) => {
return (target: object, propertyKey: string | symbol) => {
target[propertyKey] = property;
}
}
class Car {
@initCarPropertyDec('奔驰')
name!: string;
}
console.log(new Car().name)
也可以配合reflect-metadata库添加属性的元数据。(PS 装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes))
参数装饰器
装饰器参数:
- target: Object 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;
- propertyKey: string | symbol 参数名;
- parameterIndex: number 参数在函数参数列表中的索引;
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
用的比较少不展开了。
访问器装饰器
使用类似方法装饰器。
PS TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
参数:
- target: any 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;
- propertyKey: string 成员的名字;
- descriptor: PropertyDescriptor 成员的属性描述符;
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
例 官方例子
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}
总结
总结一下在TS装饰器里:
对类装饰器参数是类的构造函数,可以返回新的构造函数;
对类方法 / 属性 / 参数 / 描述符等传入参数2-3个,第一个是target静态成员-类的构造函数实例成员-类的原型对象,第二个是装饰的目标的名字,第三个是描述符。
JS装饰器(STAGE3)
一般结构
可以统一抽象为这个结构:
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer(initializer: () => void): void;
}) => Output | void;
都是两个参数,第一个是value,被装饰的值;第二个是context,关于被装饰值的上下文对象。比如kind就是装饰的类型是什么,如类class / 类方法method / 属性field / 描述符getter setter.
来分开看不同情况的下的具体上下文。
类装饰器
type ClassDecorator = (value: Function, context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}) => Function | void;
类装饰器接收正在装饰的类作为第一个参数,并且可以选择返回一个新的可调用对象(类、函数或代理)来替换它。如果返回不可调用的值,则会引发错误。
例
function logged(value, { kind, name }) {
if (kind === "class") {
return class extends value {
constructor(...args) {
super(...args);
console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`);
}
}
}
// ...
}
@logged
class C {}
new C(1);
// constructing an instance of C with arguments 1
类方法装饰器
type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
类方法装饰器接收正在装饰的方法作为第一个值,并且可以选择返回一个新方法来替换它。如果返回一个新方法,它将替换原型上的原始方法(或者在静态方法的情况下替换类本身)。如果返回任何其他类型的值,将引发错误。
例:方法装饰器的一个示例是 @logged 装饰器。该装饰器接收原始函数,并返回一个新函数,该新函数包装原始函数并在调用之前和之后记录日志。
function logged(value, { kind, name }) {
if (kind === "method") {
return function (...args) {
console.log(`starting ${name} with arguments ${args.join(", ")}`);
const ret = value.call(this, ...args);
console.log(`ending ${name}`);
return ret;
};
}
}
class C {
@logged
m(arg) {}
}
new C().m(1);
// starting m with arguments 1
// ending m
类属性装饰器
type ClassFieldDecorator = (value: undefined, context: {
kind: "field";
name: string | symbol;
access: { get(): unknown, set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => (initialValue: unknown) => unknown | void;
与方法不同,类字段在修饰时没有直接输入值。相反,用户可以选择返回一个初始化函数,该函数在分配字段时运行,接收字段的初始值并返回新的初始值。如果返回函数以外的任何其他类型的值,将会抛出错误。
例:
function logged(value, { kind, name }) {
if (kind === "field") {
return function (initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return initialValue;
};
}
// ...
}
class C {
@logged x = 1;
}
new C();
// initializing x with value 1
描述符装饰器
type ClassGetterDecorator = (value: Function, context: {
kind: "getter";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
type ClassSetterDecorator = (value: Function, context: {
kind: "setter";
name: string | symbol;
access: { set(value: unknown): void };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
也是类似类方法。
总结
JS第三阶段提案的参数统一为两个,第一个是被装饰的值,第二个是有关信息的对象。对象里的字段根据被装饰类型的不同而不同。
我个人认为,虽然参数有了各种差别,但是功能差别不大。例如类方法装饰器,只要能拿到方法本身,就可以在它前后做一些事情,并正确执行它。都是回归装饰器的概念。(只是我自己的思考,可能有没考虑到的地方,欢迎讨论!!)
参考
https://github.com/tc39/proposal-decorators#classes
https://www.tslang.cn/docs/handbook/decorators.html
https://zhuanlan.zhihu.com/p/511791205