Fast 中的 Decorator 在 typesciprt 及 js 中的差异
Fast 中的 Decorator 在 typesciprt 及 js 中的差异
结论
ts 中约定属性的装饰器语法只有两个参数type PropDecorator<T> = (target: T, key:string):void
; 而在 js 中旧的装饰器的语法var type PropDecorator<T> = (target:T, propertyName:string, descriptor: ObjectDescriptor):ObjectDescriptor|void
。对于 ts 而言,如果类的属性本身已经定义了 Descriptor, 那么装饰器的效果跟 js 中是一样的。如果属性本身并没有定义 descriptor,那么装饰器本身要么返回 descriptor,否则这个装饰器就不会体现在 descriptor 上面。
然而对于 babel 而言,首先,装饰器一定是体现在 descriptor 上面的,装饰器的体现必须是直接修改传递给装饰器的 descriptor 对象,或者装饰器返回新的 descriptor 对象。如果直接操作 property,则会被覆盖掉。其次装饰器的行为最终是体现在实例上面,而不是原型上面。
Typescript
ts 中的 decorator 只能作用于类实例的属性,方法,存取器;静态的属性,方法,存取器;构造函数;类本身
- Decorator 的语法如下
type ClassDecoratorType<T extends new (...args:any[]):{}> = (classDef:T) => T | void;
type MethodOrAccessorDecoratorType = (target: PrototypeOfClass|ConstructorFuncOfClass, name: string, descriptor:ObjectPropertyDescriptor) => ObjectPropertyDescriptor | void;
type PropertyDecoratorType = (target: PrototypeOfClass|ConstructorFuncOfClass, name:string)=>void; // 没有第三个参数
type ParameterDecoratorType = (target: PrototypeOfClass|ConstructorFuncOfClass, name:string, index:number)=>void; // 参数的Decorator只能适用于class Constructor/Method Parameter
js
目前 js 中关于 decorator 的提案处于第三阶段,我们只讨论第三阶段及前一个版本的使用。Decorator 语法是@functionName
,可以作用于类,类的属性,类的方法,属性存取器。
- Decorator 本身就是一个函数,它的新语法如下:
type Decrotor = (
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;
新版的装饰器,本身就是一个函数,它有两个参数,第一个是被修饰的对象的值(可能是 undefined),第二个是上下文。这个装饰器函数,要么返回新的被修饰的对象,要么不返回。我们也可以将定义一个函数返回上述的装饰器函数。
- 当有多个装饰器函数的时候,执行顺序:
- 计算装饰器值是从左到右,从上到下
- 调用方法的装饰器函数
- 调用类的装饰器
分别介绍一下装饰器的语法结构
// 类老的语法
type ClassDecoratorOld=(class:Function)=>void;
// 类的新语法
type ClassDecoratorNew=(value:Function,context:{
kind:'class';
name:string|undefined;
addInitializer(initializer:()=>void):void;
})=>NewFunction|void
// 方法新语法 可以返回一个新函数或者不返回
type ClassMethodDecorator=(value:Function, context:{
kind:'method';
name: string| symbol;
access:{get():unknow};
static:boolean;
private:boolean;
addInitializer(initializer:()=>void):void;
})=>Function|void
// 属性|方法装饰器
type ClassPropertyDecorator=(target:Object, name:string, decorator:Descriptor)=>Descriptor;
// 属性装饰器新语法
type ClassFieldDecorator=(value:undefined,context:{
kind:'field';
name:string|symbol;
access:{get():unknow,set(value:unknow):void};
static:boolean;
private:boolean;
}) => (initialValue:unknow)=>unknow | void;
// Get装饰器新语法
type ClassGetterDecorator = (value:Function, context:{
kind:'getter';
name: string | symbol;
access: {get(): unknow};
static: boolean;
private: boolean;
addInitializer(initializer: ()=>void):void;
}) => Function | void
// Set 装饰器新语法
type ClassSetterDecorator = (value:Function, context:{
kind:'setter';
name: string|symbol;
access:{set(value:unknow):void};
static:boolean;
private:boolean;
addInitializer(initializer: () => void):void;
}) => Function | void
类的装饰器(老的语法)
下面就是一个最简单的例子
// 老的语法
function testable(isTestable) {
return function (target) {
target.prototype.isTestable = isTestable;
};
}
// 新的语法
function newTestable(isTestable) {
return function (value, { kind, name }) {
if (kind === "class") {
return class extends value {
constructor(...args) {
super(...args);
this.isTestable = isTestable;
}
};
}
};
}
// 方法的新的语法
function replace(func, { kind }) {
return function (...args) {
const pre = func.call(this, ...args);
return pre;
};
}
// 属性的语法
function readonly(target, name, descriptor) {
return {
...descriptor, // Object.getOwnPropertyDescriptor(target,name)的返回值
writable: false,
};
}
@testable(true)
class MyClass {
@readonly
name: string;
@replace
greeting() {
return `hello ${this.name}`;
}
}
function log1(target, name, descriptor) {
const originalFunc = descriptor.value;
descriptor.value = function (...args) {
console.log(`old decorator call`);
return originalFunc.call(this, ...args);
};
}
function log2(value) {
return function (...args) {
console.log(`new decorator call`);
return value.call(this, ...args);
};
}
// 很遗憾下面的代码在最新版的edge/chrome 里面无法执行
class Test {
@log1
fun1() {
console.log("fund1");
}
@log2
fun2() {
console.log("func2");
}
}
fast 中的 Decorator 为什么只能使用 ts-loader 进行编译,babel 转码的时候究竟怎么犯错了。以代码为例
// 这个是observable 的定义的关键点
// 在这个decorator 內部,我们直接在原型上定义了Property,因为typescript的语法中属性的decorator只能接收两个参数,没有第三个descriptor.
// 这个方式的弊端是class 的属性上的装饰器只有一个会起作用
function observable(target, name) {
Object.defineProperty(target, name, {
enumerable: true,
get() {},
set(value) {},
});
}
class MyClass {
@observable prop: number;
}
- 上面的代码如果使用 tsc 进行转码,得到的代码关键点如下
var __decorator = function (decorators, target, key, desc) {
// 这个地方是从对象原型本身去desciprtor,对于class 这个值是没有的
let finaldesc =
desc === null ? Object.getOwnPropertyDescriptor(target, key) : desc;
for (let i = decorators.length - 1; i >= 0; i--) {
finaldesc = decorators[i](target, key, finaldesc) || finaldesc;
}
// 所以finaldesc 这个对象是空的,是不会重新定义Property的,所以decorator里面的原型定义得到了保留
if (finaldesc) {
Object.defineProperty(target, key, finaldesc);
}
};
class MyClass {
constructor() {}
}
__decorator([observable], MyClass.prototype, "prop", void 0);
- 如果我们使用 babel 来进行转码,babel 配置如下
const presets = [["@babel/preset-env"], ["@babel/preset-typescript"]];
const plugins = [
["@babel/plugin-proposal-decorators", { version: "legacy" }],
["@babel/plugin-proposal-class-properties"],
];
module.exports = {
presets,
plugins,
};
那么转码后的 js 关键点如下
let __descriptor;
var MyClass = function () {
// 类的属性是定义在实例上的,而不是原型上,它会在构建实例的时候将属性装饰器整合到实例的属性中去
Object.defineProperty(this, "prop", __descriptor);
};
__descriptor = __applyDecoratorDescriptor(
MyClass.prototype,
"prop",
[observable],
{
configurable: true,
enumerable: true,
writable: true,
initializer: null,
}
);
// 这个地方同样是聚合。与ts不同的是,原型上的属性装饰器会聚合到原型上,同时,约定了,修改Descriptor 的途径要么就是直接修改,要么就是返回新的Descriptor对象
__applyDecoratorDescriptor = (target, key, decorators, initialDescriptor) => {
let finaldes = initialdescriptor;
for (let i = decorators.length - 1; i >= 0; i--) {
finaldes = decorators[i](target, key, finaldes) || finaldes;
}
Object.defineProperty(target, key, finaldes);
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构