装饰器模式
问题
什么是装饰器模式?TypeScript 装饰器和设计模式中的装饰器有什么关系?前端有哪些实际应用?
答案
装饰器模式(Decorator Pattern)动态地给对象添加额外功能,是继承的一种替代方案。它通过包装对象来扩展功能,比继承更加灵活。
核心概念
特点
| 特点 | 说明 |
|---|---|
| 动态扩展 | 运行时添加功能 |
| 组合复用 | 多个装饰器可叠加 |
| 开闭原则 | 不修改原类 |
| 透明性 | 装饰后保持原接口 |
传统装饰器模式
基础实现
// 组件接口
interface Coffee {
cost(): number;
description(): string;
}
// 具体组件
class SimpleCoffee implements Coffee {
cost() {
return 10;
}
description() {
return '简单咖啡';
}
}
// 装饰器基类
abstract class CoffeeDecorator implements Coffee {
protected coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 3;
}
description() {
return this.coffee.description() + ' + 牛奶';
}
}
class SugarDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ' + 糖';
}
}
class WhipDecorator extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 5;
}
description() {
return this.coffee.description() + ' + 奶油';
}
}
// 使用 - 装饰器可以叠加
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.description(), coffee.cost()); // 简单咖啡 10
coffee = new MilkDecorator(coffee);
console.log(coffee.description(), coffee.cost()); // 简单咖啡 + 牛奶 13
coffee = new SugarDecorator(coffee);
console.log(coffee.description(), coffee.cost()); // 简单咖啡 + 牛奶 + 糖 14
coffee = new WhipDecorator(coffee);
console.log(coffee.description(), coffee.cost()); // 简单咖啡 + 牛奶 + 糖 + 奶油 19
函数装饰器
// 函数式装饰器更符合 JavaScript 风格
type AnyFunction = (...args: unknown[]) => unknown;
// 日志装饰器
function withLogging<T extends AnyFunction>(fn: T): T {
return function (...args: Parameters<T>): ReturnType<T> {
console.log(`调用 ${fn.name},参数:`, args);
const result = fn.apply(this, args);
console.log(`返回:`, result);
return result as ReturnType<T>;
} as T;
}
// 计时装饰器
function withTiming<T extends AnyFunction>(fn: T): T {
return function (...args: Parameters<T>): ReturnType<T> {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${fn.name} 执行时间: ${(end - start).toFixed(2)}ms`);
return result as ReturnType<T>;
} as T;
}
// 错误处理装饰器
function withErrorHandling<T extends AnyFunction>(fn: T): T {
return function (...args: Parameters<T>): ReturnType<T> | undefined {
try {
return fn.apply(this, args) as ReturnType<T>;
} catch (error) {
console.error(`${fn.name} 执行错误:`, error);
return undefined;
}
} as T;
}
// 使用
function calculate(a: number, b: number): number {
return a + b;
}
const decorated = withLogging(withTiming(calculate));
decorated(1, 2);
// 调用 calculate,参数: [1, 2]
// calculate 执行时间: 0.01ms
// 返回: 3
TypeScript 装饰器
类装饰器
// 类装饰器
function Component(options: { selector: string; template: string }) {
return function <T extends new (...args: unknown[]) => object>(constructor: T) {
return class extends constructor {
selector = options.selector;
template = options.template;
};
};
}
@Component({
selector: 'app-hello',
template: '<h1>Hello</h1>',
})
class HelloComponent {
name = 'Hello';
}
const hello = new HelloComponent();
console.log((hello as unknown as { selector: string }).selector); // app-hello
方法装饰器
// 方法装饰器
function Log(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: unknown[]) {
console.log(`[LOG] ${propertyKey} called with:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] ${propertyKey} returned:`, result);
return result;
};
return descriptor;
}
// Debounce 装饰器
function Debounce(delay: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let timer: ReturnType<typeof setTimeout>;
const originalMethod = descriptor.value;
descriptor.value = function (...args: unknown[]) {
clearTimeout(timer);
timer = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};
return descriptor;
};
}
// Throttle 装饰器
function Throttle(limit: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let lastCall = 0;
const originalMethod = descriptor.value;
descriptor.value = function (...args: unknown[]) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
return originalMethod.apply(this, args);
}
};
return descriptor;
};
}
class SearchService {
@Log
search(query: string) {
return `Results for: ${query}`;
}
@Debounce(300)
handleInput(value: string) {
console.log('搜索:', value);
}
@Throttle(1000)
handleScroll() {
console.log('处理滚动');
}
}
属性装饰器
// 属性验证装饰器
function Min(minValue: number) {
return function (target: object, propertyKey: string) {
let value: number;
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: number) {
if (newValue < minValue) {
throw new Error(`${propertyKey} 不能小于 ${minValue}`);
}
value = newValue;
},
});
};
}
function Max(maxValue: number) {
return function (target: object, propertyKey: string) {
let value: number;
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: number) {
if (newValue > maxValue) {
throw new Error(`${propertyKey} 不能大于 ${maxValue}`);
}
value = newValue;
},
});
};
}
function Required(target: object, propertyKey: string) {
let value: unknown;
Object.defineProperty(target, propertyKey, {
get() {
return value;
},
set(newValue: unknown) {
if (newValue === undefined || newValue === null || newValue === '') {
throw new Error(`${propertyKey} 是必填项`);
}
value = newValue;
},
});
}
class User {
@Required
name!: string;
@Min(0)
@Max(150)
age!: number;
}
参数装饰器
// 参数装饰器 - 配合元数据使用
import 'reflect-metadata';
const REQUIRED_KEY = Symbol('required');
function Required(target: object, propertyKey: string, parameterIndex: number) {
const existingRequired: number[] =
Reflect.getMetadata(REQUIRED_KEY, target, propertyKey) || [];
existingRequired.push(parameterIndex);
Reflect.defineMetadata(REQUIRED_KEY, existingRequired, target, propertyKey);
}
function Validate(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: unknown[]) {
const requiredParams: number[] =
Reflect.getMetadata(REQUIRED_KEY, target, propertyKey) || [];
for (const index of requiredParams) {
if (args[index] === undefined || args[index] === null) {
throw new Error(`参数 ${index} 是必需的`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class UserService {
@Validate
createUser(@Required name: string, age?: number) {
return { name, age };
}
}
前端实际应用
1. HOC 高阶组件(React)
import React, { ComponentType, useState, useEffect } from 'react';
// Loading 装饰器
function withLoading<P extends object>(
WrappedComponent: ComponentType<P>,
loadingText = 'Loading...'
) {
return function WithLoadingComponent(props: P & { isLoading?: boolean }) {
const { isLoading, ...rest } = props;
if (isLoading) {
return <div className="loading">{loadingText}</div>;
}
return <WrappedComponent {...(rest as P)} />;
};
}
// 错误边界装饰器
function withErrorBoundary<P extends object>(
WrappedComponent: ComponentType<P>,
FallbackComponent: ComponentType<{ error: Error }>
) {
return class WithErrorBoundary extends React.Component<
P,
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError && this.state.error) {
return <FallbackComponent error={this.state.error} />;
}
return <WrappedComponent {...this.props} />;
}
};
}
// 鉴权装饰器
function withAuth<P extends object>(WrappedComponent: ComponentType<P>) {
return function WithAuthComponent(props: P) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// 检查认证状态
const checkAuth = async () => {
const token = localStorage.getItem('token');
setIsAuthenticated(!!token);
setIsLoading(false);
};
checkAuth();
}, []);
if (isLoading) {
return <div>检查登录状态...</div>;
}
if (!isAuthenticated) {
return <div>请先登录</div>;
}
return <WrappedComponent {...props} />;
};
}
// 使用
interface UserListProps {
users: string[];
}
const UserList: React.FC<UserListProps> = ({ users }) => (
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
);
// 组合多个装饰器
const EnhancedUserList = withAuth(withLoading(UserList));
2. 请求装饰器
// API 装饰器工厂
function createApiDecorators() {
const cache = new Map<string, { data: unknown; expiry: number }>();
const pending = new Map<string, Promise<unknown>>();
// 缓存装饰器
function Cache(ttl: number) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const key = `${propertyKey}_${JSON.stringify(args)}`;
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
const result = await originalMethod.apply(this, args);
cache.set(key, { data: result, expiry: Date.now() + ttl });
return result;
};
return descriptor;
};
}
// 防抖装饰器(请求合并)
function Dedupe() {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const key = `${propertyKey}_${JSON.stringify(args)}`;
if (pending.has(key)) {
return pending.get(key);
}
const promise = originalMethod.apply(this, args);
pending.set(key, promise);
try {
return await promise;
} finally {
pending.delete(key);
}
};
return descriptor;
};
}
// 重试装饰器
function Retry(times: number, delay: number = 1000) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
let lastError: Error | undefined;
for (let i = 0; i <= times; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
if (i < times) {
console.log(`重试 ${i + 1}/${times}...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
};
return descriptor;
};
}
return { Cache, Dedupe, Retry };
}
const { Cache, Dedupe, Retry } = createApiDecorators();
class UserApi {
@Cache(30000)
@Dedupe()
async getUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
@Retry(3, 1000)
async updateUser(id: number, data: object) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return response.json();
}
}
3. 表单验证装饰器
import 'reflect-metadata';
const VALIDATORS_KEY = Symbol('validators');
interface Validator {
validate: (value: unknown) => boolean;
message: string;
}
// 验证装饰器
function Validate(validator: Validator) {
return function (target: object, propertyKey: string) {
const validators: Validator[] =
Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
validators.push(validator);
Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
};
}
// 常用验证器
function Required(message = '此字段必填') {
return Validate({
validate: (value) => value !== undefined && value !== null && value !== '',
message,
});
}
function MinLength(min: number, message = `最少 ${min} 个字符`) {
return Validate({
validate: (value) => typeof value === 'string' && value.length >= min,
message,
});
}
function MaxLength(max: number, message = `最多 ${max} 个字符`) {
return Validate({
validate: (value) => typeof value === 'string' && value.length <= max,
message,
});
}
function Email(message = '邮箱格式不正确') {
return Validate({
validate: (value) =>
typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message,
});
}
function Pattern(regex: RegExp, message: string) {
return Validate({
validate: (value) => typeof value === 'string' && regex.test(value),
message,
});
}
// 验证函数
function validate(obj: object): { valid: boolean; errors: Record<string, string[]> } {
const errors: Record<string, string[]> = {};
let valid = true;
for (const key of Object.keys(obj)) {
const validators: Validator[] =
Reflect.getMetadata(VALIDATORS_KEY, obj, key) || [];
const value = (obj as Record<string, unknown>)[key];
for (const validator of validators) {
if (!validator.validate(value)) {
valid = false;
if (!errors[key]) {
errors[key] = [];
}
errors[key].push(validator.message);
}
}
}
return { valid, errors };
}
// 使用
class UserForm {
@Required()
@MinLength(2)
@MaxLength(50)
name: string = '';
@Required()
@Email()
email: string = '';
@Required()
@MinLength(8)
@Pattern(/[A-Z]/, '必须包含大写字母')
@Pattern(/[0-9]/, '必须包含数字')
password: string = '';
}
const form = new UserForm();
form.name = 'J';
form.email = 'invalid';
form.password = 'simple';
const result = validate(form);
console.log(result);
// {
// valid: false,
// errors: {
// name: ['最少 2 个字符'],
// email: ['邮箱格式不正确'],
// password: ['最少 8 个字符', '必须包含大写字母', '必须包含数字']
// }
// }
4. 权限装饰器
// 权限装饰器
const ROLES_KEY = Symbol('roles');
function Roles(...roles: string[]) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
Reflect.defineMetadata(ROLES_KEY, roles, target, propertyKey);
return descriptor;
};
}
function checkPermission<T extends object>(
instance: T,
methodName: keyof T,
currentUserRole: string
): boolean {
const roles: string[] =
Reflect.getMetadata(ROLES_KEY, instance, methodName as string) || [];
if (roles.length === 0) {
return true; // 无权限要求
}
return roles.includes(currentUserRole);
}
// 自动权限检查装饰器
function Protected(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: unknown[]) {
const currentUser = getCurrentUser(); // 假设有这个函数
const roles: string[] =
Reflect.getMetadata(ROLES_KEY, target, propertyKey) || [];
if (roles.length > 0 && !roles.includes(currentUser.role)) {
throw new Error('权限不足');
}
return originalMethod.apply(this, args);
};
return descriptor;
}
// 模拟获取当前用户
function getCurrentUser() {
return { role: 'editor' };
}
class AdminService {
@Protected
@Roles('admin')
deleteUser(id: number) {
console.log('删除用户:', id);
}
@Protected
@Roles('admin', 'editor')
updateUser(id: number, data: object) {
console.log('更新用户:', id, data);
}
@Roles('admin', 'editor', 'viewer')
viewUser(id: number) {
console.log('查看用户:', id);
}
}
常见面试问题
Q1: 装饰器模式的优缺点?
答案:
| 优点 | 缺点 |
|---|---|
| 动态扩展功能 | 增加系统复杂度 |
| 比继承更灵活 | 多层装饰难以调试 |
| 符合开闭原则 | 装饰顺序可能影响结果 |
| 单一职责 | - |
Q2: 装饰器模式和代理模式的区别?
答案:
| 对比项 | 装饰器模式 | 代理模式 |
|---|---|---|
| 目的 | 增强功能 | 控制访问 |
| 关注点 | 添加行为 | 管理访问 |
| 叠加 | 可多层叠加 | 通常单层 |
| 透明度 | 客户端知道装饰 | 对客户端透明 |
// 装饰器 - 增强功能
const decorated = withLogging(withCache(originalFn));
// 代理 - 控制访问
const proxy = new Proxy(obj, {
get(target, prop) {
checkPermission();
return target[prop];
},
});
Q3: TypeScript 装饰器执行顺序?
答案:
function ClassDecorator1() {
console.log('ClassDecorator1');
return function (target: Function) {
console.log('ClassDecorator1 执行');
};
}
function MethodDecorator1() {
console.log('MethodDecorator1');
return function (target: object, key: string, desc: PropertyDescriptor) {
console.log('MethodDecorator1 执行');
};
}
function MethodDecorator2() {
console.log('MethodDecorator2');
return function (target: object, key: string, desc: PropertyDescriptor) {
console.log('MethodDecorator2 执行');
};
}
@ClassDecorator1()
class Test {
@MethodDecorator1()
@MethodDecorator2()
method() {}
}
// 输出:
// MethodDecorator1
// MethodDecorator2
// MethodDecorator2 执行 (从下往上)
// MethodDecorator1 执行
// ClassDecorator1
// ClassDecorator1 执行
执行规则:
- 工厂函数从上往下执行
- 装饰器从下往上应用
- 类装饰器最后执行
Q4: 如何在 React 中使用装饰器?
答案:
// 方式1: HOC(推荐)
function withCounter<P extends { count: number }>(
WrappedComponent: React.ComponentType<P>
) {
return function WithCounter(props: Omit<P, 'count'>) {
const [count, setCount] = useState(0);
return <WrappedComponent {...(props as P)} count={count} />;
};
}
// 方式2: 自定义 Hook(推荐)
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
return { count, increment, decrement };
}
// 方式3: 类组件装饰器(旧版)
@connect(mapStateToProps) // Redux connect
@withRouter // React Router
class MyComponent extends React.Component {
// ...
}