JavaScript 装饰器(Decorators)提案:基于元编程的函数式转换
各位同仁,各位技术爱好者,大家好!
今天,我们将深入探讨 JavaScript 语言中一个极具变革性的提案:装饰器(Decorators)。这个提案,目前已达到 TC39 Stage 3 阶段,距离最终成为语言标准仅一步之遥。它为 JavaScript 带来了强大的元编程能力,并以一种函数式转换的优雅姿态,极大地提升了我们代码的表达力、可维护性和复用性。
1. 装饰器:何以为代码赋能?
想象一下,你正在构建一个复杂的系统,其中有许多类和方法。你可能会遇到一些横切关注点(Cross-Cutting Concerns),例如:
- 日志记录: 记录方法的调用、参数和返回值。
- 权限校验: 确保用户有权执行某个操作。
- 性能监控: 测量方法的执行时间。
- 数据验证: 在方法执行前验证输入参数。
- 缓存: 缓存方法的计算结果以提高性能。
- 依赖注入: 自动为类的属性提供所需的依赖服务。
在没有装饰器的情况下,我们通常会采用以下几种模式来处理这些问题:
- 手动包装: 在每个方法内部或外部手动添加逻辑。这会导致大量重复代码,难以维护。
- 继承: 创建基类来封装通用逻辑。但这会引入复杂的继承链,且JavaScript单继承的限制使得这种方式很快就会捉襟见肘。
- 高阶函数(Higher-Order Functions – HOFs): 将函数作为参数或返回值的函数。这对于纯函数非常有效,但对于类的方法,尤其是需要访问
this上下文的情况,会变得相对繁琐。 - 高阶组件(Higher-Order Components – HOCs): 在 React 等框架中常见,用于包装组件以增强其功能。这是一种针对特定框架的模式。
- 代理(Proxies): 在运行时拦截对象操作。虽然强大,但通常用于更通用的运行时行为拦截,而不是声明式地修改特定成员。
这些方法各有优劣,但共同的痛点是:它们往往不够声明式,或者会侵入到业务逻辑中,使得代码难以阅读和理解。装饰器的出现,正是为了解决这些痛点,提供一种优雅、声明式、非侵入式的方式,在不修改原有代码结构的前提下,对类及其成员进行增强和转换。
那么,什么是 JavaScript 装饰器?
从最核心的层面来看,装饰器是一种特殊的声明,可以附加到类、方法、访问器(getter/setter)、属性(字段)以及自动访问器上。它本质上是一个函数,这个函数会在被装饰的目标定义时被调用,并接收关于目标的信息(元数据)。然后,它可以返回一个新的目标,或者修改原有的目标行为,从而实现对目标代码的“装饰”或“转换”。
2. 元编程与函数式转换:装饰器的核心思想
理解装饰器,必须抓住其背后的两大基石:元编程(Metaprogramming)和函数式转换(Functional Transformation)。
2.1 元编程:代码即数据
元编程是指编写能够操作其他代码的代码。它允许程序在运行时检查、修改甚至生成自身的结构和行为。在 JavaScript 中,我们已经通过 Proxy 和 Reflect API 接触到了运行时元编程。而装饰器则将元编程提升到了一个新的层次——定义时元编程。
当你在一个类、方法或属性上应用装饰器时,JavaScript 引擎会在这些目标被定义(而不是运行时被调用)的阶段,将目标的信息传递给装饰器函数。装饰器函数接收这些信息,并可以决定如何修改目标。这种在代码定义时介入并改变其行为的能力,正是元编程的体现。
例如,一个装饰器可以:
- 读取一个方法的名称。
- 修改一个属性的默认值。
- 替换一个类的构造函数。
- 为一个方法添加额外的逻辑(例如在执行前和执行后)。
通过这种方式,我们得以在更高的抽象层面思考和操作代码,将重复的、通用的逻辑从业务代码中抽离出来,实现代码的“自我改造”。
2.2 函数式转换:纯净与组合
装饰器本质上是函数。它们接收一个输入(被装饰的目标及其上下文),并返回一个输出(新的目标或对原目标的修改)。这种“输入 -> 转换 -> 输出”的模式,完美契合了函数式编程的思想。
一个理想的装饰器,应当是一个纯函数:给定相同的输入,总是返回相同的输出,并且不产生任何副作用。然而,在实际应用中,装饰器的目的往往就是产生副作用(例如,修改类的行为)。因此,我们更强调其“转换”的特性:它将一个实体(类、方法等)转换成另一个具有增强功能的实体。
这种转换可以是:
- 包装(Wrapping): 用一个新函数包装原有方法,添加前置/后置逻辑。
- 替换(Replacing): 完全用一个新函数或新类替换原有目标。
- 修改描述符(Modifying Descriptors): 调整属性的
writable、enumerable等特性。
装饰器可以像乐高积木一样进行组合。多个装饰器可以应用于同一个目标,形成一个转换管道。这种组合能力使得我们可以将复杂的行为分解为一系列简单的、可复用的装饰器,并通过声明式的方式将它们组装起来。
// 假设有日志和权限两个装饰器
@log('debug')
@authorize('admin')
class UserService {
getUser(id: string) {
// ... 获取用户逻辑
}
}
这里,getUser 方法首先会被 @authorize('admin') 装饰器处理,然后其结果再被 @log('debug') 装饰器处理。这种从内到外(或从下到上)的函数式组合,使得代码的意图一目了然。
3. 装饰器语法与应用目标
装饰器的语法非常简洁:使用 @ 符号紧跟装饰器函数的名称,放置在被装饰目标之前。如果装饰器需要参数,它必须是一个返回装饰器函数的函数(即装饰器工厂)。
// 简单装饰器
@decoratorName
class MyClass {}
// 装饰器工厂
@decoratorFactory(arg1, arg2)
class AnotherClass {}
装饰器可以应用于以下几种目标:
-
类装饰器(Class Decorators): 装饰整个类。
@sealed class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } -
方法装饰器(Method Decorators): 装饰类的方法。
class MyService { @logMethod getData(id: string) { // ... } } -
属性装饰器(Property Decorators,或称字段装饰器 Field Decorators): 装饰类的属性(字段)。
class User { @maxLength(20) username: string; } -
访问器装饰器(Accessor Decorators): 装饰类的
getter或setter。class Point { private _x: number; private _y: number; constructor(x: number, y: number) { this._x = x; this._y = y; } @immutable get x() { return this._x; } set x(value: number) { this._x = value; } @immutable get y() { return this._y; } set y(value: number) { this._y = value; } } -
自动访问器装饰器(Auto-Accessor Decorators): 装饰使用
accessor关键字声明的自动访问器字段(ES2022+ 提案)。class Counter { @observable accessor count = 0; // 自动生成 getter 和 setter }
装饰器的求值顺序:
当一个目标上应用了多个装饰器时,它们的求值顺序是从上到下,但应用顺序(执行顺序)是从下到上(或从内到外)。这意味着最靠近目标的装饰器会先被执行,其返回的结果会作为下一个装饰器的输入。
@D1
@D2
class MyClass {
@M1
@M2
myMethod() {}
}
求值顺序:D1 -> D2 -> M1 -> M2。
应用顺序:M2 作用于 myMethod,然后 M1 作用于 M2 的结果;D2 作用于 MyClass,然后 D1 作用于 D2 的结果。
4. 深入剖析装饰器种类与签名(TC39 Stage 3 提案)
这是理解现代 JavaScript 装饰器最关键的部分。TC39 提案经历了多次迭代,目前的 Stage 3 提案与早期版本(包括 TypeScript 早期实验性支持的版本)有显著差异。核心变化在于引入了 context 对象,并统一了不同类型装饰器的签名模式。
所有的装饰器函数都接收两个参数:value 和 context。
value:被装饰目标的值。具体类型取决于被装饰目标的种类。context:一个包含元数据的对象,描述了被装饰目标的类型、名称、是否静态、是否私有等信息,并提供了一些实用工具(如addInitializer)。
4.1 类装饰器(Class Decorators)
签名:
(value: Function, context: {
kind: 'class';
name: string | symbol;
addInitializer(initializer: (this: Function) => void): void;
}) => Function | void;
value:被装饰的类构造函数本身。context.kind: 始终为'class'。context.name: 类的名称。context.addInitializer: 一个函数,允许你在类定义完成之后,但在类的实例首次创建之前,添加一个初始化函数。这个初始化函数会在this绑定到类本身的情况下执行。
返回:
- 可以返回一个新的类构造函数,以替换原始类。
- 可以返回
void(或undefined),表示不替换原始类。
示例:为类添加静态属性和方法
function registerService(name: string) {
return function <T extends { new(...args: any[]): {} }>(
value: T,
context: ClassDecoratorContext<T>
) {
if (context.kind !== 'class') {
throw new Error('registerService can only decorate classes.');
}
// 添加一个静态方法
context.addInitializer(function(this: T) {
console.log(`Service ${name} initialized for class ${context.name as string}.`);
});
return class extends value {
static serviceName = name;
static getInstance(...args: any[]): InstanceType<T> {
console.log(`Getting instance of ${name}`);
return new this(...args) as InstanceType<T>;
}
};
};
}
@registerService('UserService')
class UserService {
private users: { id: string; name: string }[] = [];
constructor() {
this.users = [{ id: '1', name: 'Alice' }];
console.log(`UserService instance created.`);
}
getUser(id: string) {
return this.users.find(u => u.id === id);
}
}
// 通过装饰器添加的静态方法和属性
console.log(UserService.serviceName); // 输出: UserService
const service = UserService.getInstance(); // 输出: Getting instance of UserService, UserService instance created.
console.log(service.getUser('1')); // 输出: { id: '1', name: 'Alice' }
4.2 方法装饰器(Method Decorators)
签名:
(value: Function, context: {
kind: 'method';
name: string | symbol;
static: boolean;
private: boolean;
addInitializer(initializer: (this: Class) => void): void;
}) => Function | void;
value:被装饰的方法函数本身。context.kind: 始终为'method'。context.name: 方法的名称。context.static:true如果是静态方法,false如果是实例方法。context.private:true如果是私有方法,false如果是公共方法。context.addInitializer: 同类装饰器,用于在类定义时进行方法相关的初始化。
返回:
- 可以返回一个新的函数,以替换原始方法。
- 可以返回
void,表示不替换原始方法。
示例:记录方法调用日志
function logMethod(
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') {
throw new Error('logMethod can only decorate methods.');
}
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`[${methodName}] called with arguments: ${JSON.stringify(args)}`);
const result = value.apply(this, args);
console.log(`[${methodName}] returned: ${JSON.stringify(result)}`);
return result;
}
return replacementMethod;
}
class Calculator {
@logMethod
add(a: number, b: number): number {
return a + b;
}
@logMethod
subtract(a: number, b: number): number {
return a - b;
}
}
const calc = new Calculator();
calc.add(5, 3);
// 输出:
// [add] called with arguments: [5,3]
// [add] returned: 8
calc.subtract(10, 4);
// 输出:
// [subtract] called with arguments: [10,4]
// [subtract] returned: 6
4.3 属性装饰器(Property Decorators / Field Decorators)
签名:
(value: undefined, context: {
kind: 'field';
name: string | symbol;
static: boolean;
private: boolean;
addInitializer(initializer: (this: Class) => void): void;
}) => ((initialValue: unknown) => unknown) | void;
value:对于字段装饰器,value始终是undefined。这是因为字段的初始化发生在类构造函数的内部,在装饰器运行之后。context.kind: 始终为'field'。context.name: 字段的名称。context.static:true如果是静态字段,false如果是实例字段。context.private:true如果是私有字段,false如果是公共字段。context.addInitializer: 同上。
返回:
- 可以返回一个初始化函数
((initialValue) => newValue)。这个函数在实例创建时,字段被赋值时调用,接收原始的初始值(如果有的话),并返回新的初始值。 - 可以返回
void,表示不修改字段的初始化行为。
示例:为属性设置默认值或进行类型转换
function defaultIfUndefined<T>(defaultValue: T) {
return function (
value: undefined,
context: ClassFieldDecoratorContext<any, T | undefined>
) {
if (context.kind !== 'field') {
throw new Error('defaultIfUndefined can only decorate fields.');
}
return function (initialValue: T | undefined) {
return initialValue === undefined ? defaultValue : initialValue;
};
};
}
class UserProfile {
@defaultIfUndefined('Guest')
username: string;
@defaultIfUndefined(0)
age: number;
email: string = '[email protected]'; // 显式初始化不会被覆盖
constructor(username?: string, age?: number) {
this.username = username!;
this.age = age!;
}
}
const user1 = new UserProfile();
console.log(user1.username); // 输出: Guest
console.log(user1.age); // 输出: 0
console.log(user1.email); // 输出: [email protected]
const user2 = new UserProfile('Alice', 30);
console.log(user2.username); // 输出: Alice
console.log(user2.age); // 输出: 30
4.4 访问器装饰器(Accessor Decorators)
签名:
(value: { get: Function; set: Function; }, context: {
kind: 'accessor';
name: string | symbol;
static: boolean;
private: boolean;
addInitializer(initializer: (this: Class) => void): void;
}) => { get?: Function; set?: Function; } | void;
value:一个对象,包含原始的get和set函数。context.kind: 始终为'accessor'。context.name: 访问器的名称。context.static:true如果是静态访问器,false如果是实例访问器。context.private:true如果是私有访问器,false如果是公共访问器。context.addInitializer: 同上。
返回:
- 可以返回一个包含新
get和/或set函数的对象,以替换原始的访问器。 - 可以返回
void,表示不替换原始访问器。
示例:只读访问器
function readonlyAccessor(
value: { get: Function; set: Function; },
context: ClassAccessorDecoratorContext
) {
if (context.kind !== 'accessor') {
throw new Error('readonlyAccessor can only decorate accessors.');
}
const accessorName = String(context.name);
console.log(`Making accessor ${accessorName} readonly.`);
return {
get: value.get,
set: function (this: any, newValue: any) {
console.warn(`Attempted to set readonly accessor '${accessorName}' to '${newValue}'. Operation ignored.`);
// 不执行原始的 set 函数
}
};
}
class Product {
private _price: number;
private _name: string;
constructor(name: string, price: number) {
this._name = name;
this._price = price;
}
@readonlyAccessor
get price(): number {
return this._price;
}
set price(value: number) {
this._price = value;
}
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const product = new Product('Laptop', 1200);
console.log(product.price); // 输出: 1200
product.price = 1500; // 尝试修改只读属性
// 输出: Attempted to set readonly accessor 'price' to '1500'. Operation ignored.
console.log(product.price); // 输出: 1200 (未改变)
product.name = 'Gaming Laptop'; // 普通属性可修改
console.log(product.name); // 输出: Gaming Laptop
4.5 自动访问器装饰器(Auto-Accessor Decorators)
自动访问器是 ES2022+ 提案中的一个新特性,允许使用 accessor 关键字声明一个字段,它会自动生成对应的 getter 和 setter。装饰器可以作用于这个自动生成的访问器。
签名:
(value: { get: Function; set: Function; init: (instance: Class) => any; }, context: {
kind: 'auto-accessor';
name: string | symbol;
static: boolean;
private: boolean;
addInitializer(initializer: (this: Class) => void): void;
}) => {
get?: Function;
set?: Function;
init?: (instance: Class, initialValue: any) => any;
} | void;
value:一个对象,包含原始的get、set函数和一个init函数。init函数用于在实例创建时设置自动访问器的初始值。context.kind: 始终为'auto-accessor'。context.name: 自动访问器的名称。context.static:true如果是静态自动访问器,false如果是实例自动访问器。context.private:true如果是私有自动访问器,false如果是公共自动访问器。context.addInitializer: 同上。
返回:
- 可以返回一个包含新的
get、set和/或init函数的对象。 - 可以返回
void,表示不替换原始自动访问器。
示例:在自动访问器上实现数据绑定/观察者模式
type Listener<T> = (newValue: T, oldValue: T) => void;
function observable<T>(
value: { get: Function; set: Function; init: (instance: any) => T; },
context: ClassAutoAccessorDecoratorContext<any, T>
) {
if (context.kind !== 'auto-accessor') {
throw new Error('observable can only decorate auto-accessors.');
}
const accessorName = String(context.name);
// 为每个实例添加一个私有属性来存储监听器
context.addInitializer(function(this: any) {
if (!this.__listeners__) {
this.__listeners__ = new Map<string | symbol, Listener<any>[]>();
}
});
return {
get: function(this: any) {
return value.get.call(this);
},
set: function(this: any, newValue: T) {
const oldValue = value.get.call(this);
if (newValue !== oldValue) {
value.set.call(this, newValue);
const listeners = this.__listeners__.get(accessorName) || [];
listeners.forEach((listener: Listener<T>) => listener(newValue, oldValue));
}
},
init: function(this: any, initialValue: T) {
// 可以在这里注册一个方法来添加监听器
this[`on${String(accessorName).charAt(0).toUpperCase() + String(accessorName).slice(1)}Change`] = (
listener: Listener<T>
) => {
if (!this.__listeners__.has(accessorName)) {
this.__listeners__.set(accessorName, []);
}
this.__listeners__.get(accessorName)!.push(listener);
};
return initialValue;
}
};
}
class Store {
@observable
accessor count = 0;
@observable
accessor message = "Hello";
}
const store = new Store();
// 注册监听器
store.onCountChange((newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
store.onMessageChange((newValue, oldValue) => {
console.log(`Message changed from "${oldValue}" to "${newValue}"`);
});
store.count = 1; // 输出: Count changed from 0 to 1
store.count = 1; // 不会触发,因为值未改变
store.count = 5; // 输出: Count changed from 1 to 5
store.message = "World"; // 输出: Message changed from "Hello" to "World"
4.6 装饰器种类与签名总结
| 装饰器类型 | context.kind |
value 类型 |
返回值类型 | 主要用途 |
|---|---|---|---|---|
| 类装饰器 | 'class' |
Function (类构造函数) |
Function | void (新类或不替换) |
修改类定义、添加静态成员、注册类、工厂模式 |
| 方法装饰器 | 'method' |
Function (方法函数) |
Function | void (新方法或不替换) |
增强方法行为、日志、缓存、权限校验、限流 |
| 属性装饰器 | 'field' |
undefined |
((initialValue: unknown) => unknown) | void (初始化函数或不替换) |
设置默认值、类型转换、数据绑定、ORM映射 |
| 访问器装饰器 | 'accessor' |
{ get: Function; set: Function; } |
{ get?: Function; set?: Function; } | void (新访问器或不替换) |
缓存 getter、校验 setter、只读属性 |
| 自动访问器装饰器 | 'auto-accessor' |
{ get: Function; set: Function; init: Function; } |
{ get?: Function; set?: Function; init?: Function; } | void (新自动访问器或不替换) |
数据绑定、响应式属性、属性初始化逻辑的增强 |
所有 context 对象都包含 name, static, private(对于类装饰器,static 和 private 不适用),以及 addInitializer 方法。
5. 装饰器工厂与组合
5.1 装饰器工厂(Decorator Factories)
当装饰器需要接收参数时,它必须是一个装饰器工厂。装饰器工厂是一个函数,它接收参数,然后返回一个真正的装饰器函数。
function log(level: 'info' | 'warn' | 'error') {
return function (
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') {
throw new Error('log can only decorate methods.');
}
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console[level](`[${methodName}] (${level.toUpperCase()}) called with: ${JSON.stringify(args)}`);
const result = value.apply(this, args);
console[level](`[${methodName}] (${level.toUpperCase()}) returned: ${JSON.stringify(result)}`);
return result;
};
};
}
class MyService {
@log('info')
fetchData(id: string) {
return { id, data: `Data for ${id}` };
}
@log('error')
processError(err: Error) {
throw err;
}
}
const service = new MyService();
service.fetchData('123');
try {
service.processError(new Error('Something went wrong'));
} catch (e) {
// 错误被抛出,但日志已记录
}
5.2 装饰器组合(Decorator Composition)
多个装饰器可以应用于同一个目标,它们会像洋葱一样层层包裹。外层装饰器会接收内层装饰器处理后的结果。执行顺序是从下到上(或从内到外)。
// 简单模拟一个权限校验装饰器
function authorize(role: string) {
return function (
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') throw new Error('authorize can only decorate methods.');
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
// 假设这里有获取当前用户角色的逻辑
const currentUserRole = 'admin'; // 模拟当前用户角色
if (currentUserRole !== role) {
throw new Error(`Permission denied: User is not a ${role}`);
}
console.log(`[${methodName}] Authorization successful for role: ${role}`);
return value.apply(this, args);
};
};
}
class AdminPanel {
@log('info')
@authorize('admin')
deleteUser(userId: string) {
console.log(`Deleting user: ${userId}`);
return `User ${userId} deleted.`;
}
@log('warn')
@authorize('moderator')
banUser(userId: string) {
console.log(`Banning user: ${userId}`);
return `User ${userId} banned.`;
}
}
const admin = new AdminPanel();
try {
admin.deleteUser('user123');
// 输出:
// [deleteUser] Authorization successful for role: admin
// [deleteUser] (INFO) called with: ["user123"]
// Deleting user: user123
// [deleteUser] (INFO) returned: "User user123 deleted."
} catch (e: any) {
console.error(e.message);
}
try {
admin.banUser('user456'); // 模拟当前用户是 admin,无法执行 moderator 权限的操作
} catch (e: any) {
console.error(e.message); // 输出: Permission denied: User is not a moderator
}
在这个例子中,@authorize('admin') 先于 @log('info') 执行。如果授权失败,log 装饰器甚至不会有机会执行被装饰的方法。
6. 实用场景与高级模式
装饰器强大的声明式能力,使其在许多领域都有广泛应用。
6.1 日志与调试
除了上面演示的方法日志,我们还可以实现更复杂的日志策略,例如记录方法执行时间:
function measureExecutionTime(
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') throw new Error('measureExecutionTime can only decorate methods.');
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
const start = performance.now();
const result = value.apply(this, args);
const end = performance.now();
console.log(`Method '${methodName}' executed in ${end - start}ms.`);
return result;
};
}
class ReportGenerator {
@measureExecutionTime
generateLargeReport(data: any[]): string {
// 模拟耗时操作
let result = '';
for (let i = 0; i < 1000000; i++) {
result += String(Math.random());
}
return `Report for ${data.length} items.`;
}
}
const generator = new ReportGenerator();
generator.generateLargeReport([1, 2, 3]);
// 输出: Method 'generateLargeReport' executed in XXXms.
6.2 验证(Validation)
对方法参数或类属性进行验证是常见需求。
function validate(validatorFn: (...args: any[]) => boolean, errorMessage: string = 'Validation failed.') {
return function (
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') throw new Error('validate can only decorate methods.');
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
if (!validatorFn(...args)) {
throw new Error(`[${methodName}] Validation Error: ${errorMessage}`);
}
return value.apply(this, args);
};
};
}
function minLength(len: number) {
return function (
value: undefined,
context: ClassFieldDecoratorContext<any, string>
) {
if (context.kind !== 'field') throw new Error('minLength can only decorate fields.');
const fieldName = String(context.name);
context.addInitializer(function(this: any) {
Object.defineProperty(this, fieldName, {
get: () => this[`_decorated_${fieldName}`],
set: (val: string) => {
if (val.length < len) {
throw new Error(`Field '${fieldName}' must have at least ${len} characters.`);
}
this[`_decorated_${fieldName}`] = val;
},
enumerable: true,
configurable: true
});
});
};
}
class UserInput {
@minLength(5)
username: string = '';
constructor(username: string) {
this.username = username;
}
@validate((email: string) => /^[^s@]+@[^s@]+.[^s@]+$/.test(email), 'Invalid email format')
sendEmail(email: string) {
console.log(`Sending email to ${email}`);
}
}
try {
const user = new UserInput('JohnDoe');
user.sendEmail('[email protected]'); // Works
user.sendEmail('invalid-email'); // Throws error
} catch (e: any) {
console.error(e.message); // [sendEmail] Validation Error: Invalid email format
}
try {
const user2 = new UserInput('bob'); // Throws error
} catch (e: any) {
console.error(e.message); // Field 'username' must have at least 5 characters.
}
注意:字段装饰器实现验证通常需要结合 addInitializer 和 Object.defineProperty 来劫持 setter,因为 value 参数是 undefined。这比方法装饰器稍微复杂一些。
6.3 缓存(Memoization)
缓存方法的结果以避免重复计算是性能优化的常用手段。
function memoize(
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') throw new Error('memoize can only decorate methods.');
const methodName = String(context.name);
const cache = new Map<string, any>();
return function (this: any, ...args: any[]) {
const cacheKey = JSON.stringify(args); // 简单的缓存键生成
if (cache.has(cacheKey)) {
console.log(`[${methodName}] Cache hit for ${cacheKey}`);
return cache.get(cacheKey);
}
console.log(`[${methodName}] Cache miss for ${cacheKey}, calculating...`);
const result = value.apply(this, args);
cache.set(cacheKey, result);
return result;
};
}
class Fibonacci {
@memoize
calculate(n: number): number {
if (n <= 1) return n;
return this.calculate(n - 1) + this.calculate(n - 2);
}
}
const fib = new Fibonacci();
console.log(fib.calculate(10)); // 计算并缓存
console.log(fib.calculate(10)); // 从缓存获取
console.log(fib.calculate(5)); // 计算并缓存
console.log(fib.calculate(5)); // 从缓存获取
6.4 依赖注入(Dependency Injection – DI)
装饰器是实现轻量级 DI 的理想工具,尤其是在 TypeScript 环境中。
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[ConsoleLogger] ${message}`);
}
}
// 模拟一个简单的DI容器
const container = new Map<string, any>();
container.set('ILogger', new ConsoleLogger());
function inject(token: string) {
return function (
value: undefined,
context: ClassFieldDecoratorContext<any, any>
) {
if (context.kind !== 'field') throw new Error('inject can only decorate fields.');
const fieldName = String(context.name);
// 在类初始化时从容器中获取依赖
context.addInitializer(function(this: any) {
if (!container.has(token)) {
throw new Error(`Dependency for token '${token}' not found.`);
}
this[fieldName] = container.get(token);
});
};
}
class UserService {
@inject('ILogger')
private logger!: ILogger; // 使用 ! 告诉 TypeScript 编译器这个属性会在运行时被初始化
getUser(id: string) {
this.logger.log(`Fetching user with ID: ${id}`);
return { id, name: 'Injected User' };
}
}
const userService = new UserService();
console.log(userService.getUser('456'));
// 输出:
// [ConsoleLogger] Fetching user with ID: 456
// { id: '456', name: 'Injected User' }
6.5 框架集成(ORM, Routing)
许多框架(如 Angular、NestJS)已经广泛使用装饰器来定义组件、服务、路由、数据库模型等。虽然它们的实现可能基于旧版或自定义的装饰器语法,但核心思想是相通的。
// 模拟一个简化的ORM Column装饰器
function Column(options?: { name?: string; type?: string; primary?: boolean }) {
return function (
value: undefined,
context: ClassFieldDecoratorContext<any, any>
) {
if (context.kind !== 'field') throw new Error('Column can only decorate fields.');
const fieldName = String(context.name);
context.addInitializer(function(this: any) {
// 在类的原型上存储元数据,供ORM框架读取
const metadata = Reflect.getMetadata('columns', this.constructor) || [];
metadata.push({
propertyKey: fieldName,
columnName: options?.name || fieldName,
type: options?.type || 'string',
primary: options?.primary || false,
});
Reflect.defineMetadata('columns', metadata, this.constructor);
});
};
}
// 模拟一个简化的路由装饰器
function Get(path: string) {
return function (
value: Function,
context: ClassMethodDecoratorContext
) {
if (context.kind !== 'method') throw new Error('Get can only decorate methods.');
const methodName = String(context.name);
context.addInitializer(function(this: any) {
const routes = Reflect.getMetadata('routes', this.constructor) || [];
routes.push({
method: 'GET',
path: path,
handler: methodName,
});
Reflect.defineMetadata('routes', routes, this.constructor);
});
};
}
// 需要引入 'reflect-metadata' 库并在 tsconfig.json 中启用
// import 'reflect-metadata';
class UserEntity {
@Column({ primary: true, type: 'uuid' })
id!: string;
@Column({ name: 'user_name' })
name!: string;
@Column()
email!: string;
}
class UserController {
@Get('/users')
getAllUsers() {
return 'List of all users';
}
@Get('/users/:id')
getUserById(id: string) {
return `User with ID: ${id}`;
}
}
// 模拟框架启动时读取元数据
// console.log(Reflect.getMetadata('columns', UserEntity));
// console.log(Reflect.getMetadata('routes', UserController));
注意: 上述示例中的 Reflect.getMetadata 和 Reflect.defineMetadata 需要 reflect-metadata 库的支持,通常与 TypeScript 结合使用。这展示了装饰器与元数据反射机制的紧密结合。
7. 装饰器与现有模式的对比
| 特性/模式 | 描述 | 优点 | 缺点 | 装饰器对比 |
|---|---|---|---|---|
| 高阶函数 (HOF) | 函数接收函数作为参数或返回函数。 | 灵活,可组合,函数式编程核心。 | 对于类方法,需要手动绑定 this,语法冗长。 |
装饰器是 HOF 的语法糖,将 HOF 模式应用于类和其成员,更加声明式和简洁。 |
| 高阶组件 (HOC) | React 中常见,函数接收组件作为参数并返回新组件。 | 逻辑复用,关注点分离。 | “HOC地狱”:多层嵌套导致组件树复杂,调试困难;命名冲突。 | 装饰器提供更简洁的 HOC 语法(如果用于类组件),可读性更高。对于函数式组件,HOC 仍是主流。 |
| 混入 (Mixins) | 对象或类通过复制属性和方法来“混入”其他对象的行为。 | 简单实现代码复用。 | 来源不清晰,命名冲突,this 上下文问题,难以追踪行为。 |
装饰器提供更安全的组合方式,通过显式装饰而非隐式属性复制来增强行为。 |
| 代理 (Proxies) | 在运行时拦截对象操作(如属性访问、方法调用)。 | 极其强大和灵活,可实现深度运行时行为控制。 | 作用于运行时,性能开销,调试困难,无法修改私有成员。 | 装饰器作用于定义时,更适合声明式地修改结构和行为。装饰器可以内部使用 Proxy 来实现运行时行为增强。 |
| 继承 | 通过 extends 关键字实现类之间的层次结构。 |
代码复用,多态性。 | 单继承限制,继承链过深导致代码僵硬,“脆弱的基类问题”。 | 装饰器提供横向扩展能力,避免深层继承,支持多重行为叠加。 |
8. 工具链与未来展望
8.1 Babel 支持
由于装饰器仍处于 Stage 3 阶段,浏览器原生支持尚需时日。目前,我们需要通过 Babel 进行转译。
-
旧版/实验性装饰器: 如果你使用 TypeScript 早期版本或 Babel 的旧插件
@babel/plugin-proposal-decorators且未配置version,它默认会使用旧版legacy模式。// .babelrc { "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], "@babel/plugin-proposal-class-properties" // legacy decorators usually need this too ] }这种模式与当前 TC39 提案差异较大。
-
TC39 Stage 3 提案支持: 为了使用最新的提案语法和语义,需要指定
version: "2023-11"。// .babelrc { "plugins": [ ["@babel/plugin-proposal-decorators", { "version": "2023-11" }], ["@babel/plugin-transform-class-properties", { "loose": false }], // 确保 class fields 也符合标准 ["@babel/plugin-transform-private-methods", { "loose": false }], ["@babel/plugin-transform-private-property-in-object", { "loose": false }] ] }请注意,为了正确转译私有字段和方法,可能需要额外的 Babel 插件。
8.2 TypeScript 支持
TypeScript 长期以来都有自己的实验性装饰器实现(通过 experimentalDecorators 编译器选项),这个实现是基于 TC39 提案的早期版本。这意味着在 TypeScript 中使用 @ 语法,其行为和签名与当前 Stage 3 提案是不同的。
TypeScript 实验性装饰器(旧版)的特点:
- 类装饰器:
function (target: Function) - 方法/访问器装饰器:
function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) - 属性装饰器:
function (target: Object, propertyKey: string | symbol)
如何配置 TypeScript 以支持 TC39 Stage 3 装饰器:
TypeScript 5.2+ 版本开始支持 Stage 3 装饰器。你需要在 tsconfig.json 中配置:
// tsconfig.json
{
"compilerOptions": {
"target": "es2022", // 或更高版本
"module": "esnext",
"experimentalDecorators": false, // 禁用旧的实验性装饰器
"useDefineForClassFields": true, // 确保类字段语义与标准一致
"emitDecoratorMetadata": true, // 如果需要 reflect-metadata
"lib": ["es2022", "dom"], // 确保包含所需的库
// 启用新的装饰器语法
"moduleResolution": "node"
}
}
重要提示:
experimentalDecorators: false是关键,它会禁用旧的实验性装饰器。- 你需要确保你的 TypeScript 版本至少是 5.2。
- 即使 TypeScript 编译器支持,运行时仍需要 Babel 或其他转译器来处理,直到浏览器原生支持。
8.3 展望未来
一旦装饰器最终成为语言标准,它将对 JavaScript 生态系统产生深远影响:
- 框架和库的演进: 现有框架(如 Angular、NestJS)将能够迁移到标准装饰器,提供更统一、更强大的 API。新的框架可能会围绕装饰器构建其核心功能。
- 代码简洁性: 开发者能够以更少的代码实现更复杂的功能,提高代码可读性和可维护性。
- 元编程的普及: 更多的开发者将接触并利用元编程的能力,编写更灵活、更可扩展的应用程序。
- 跨领域应用: 从 Web 开发到 Node.js 后端,再到桌面应用(如 Electron),装饰器都将成为增强代码能力的利器。
9. 使用装饰器的考量与最佳实践
尽管装饰器功能强大,但也需审慎使用。
- 适度使用: 装饰器能够提供抽象,但也可能隐藏复杂性。不要过度使用,以免代码变得难以理解和调试。
- 清晰的职责: 每个装饰器都应该有清晰、单一的职责。这有助于提高可复用性和可测试性。
- 避免副作用: 尽量使装饰器函数本身是纯净的。如果必须引入副作用(如修改类),请确保这些副作用是可控且文档化的。
- 测试性: 被装饰的代码可能需要专门的测试策略。考虑如何测试装饰器本身,以及被装饰后的代码。
- 性能影响: 装饰器主要在定义时执行,对运行时性能影响通常很小。但如果装饰器内部执行了复杂的计算或创建了昂贵的运行时代理,可能会有性能考量。
- 文档和约定: 对于自定义的装饰器,务必提供清晰的文档说明其用途、参数和行为。在团队内部建立使用装饰器的约定。
- 错误处理: 装饰器内部的错误处理同样重要。确保在发生错误时能够提供有意义的反馈。