你是怎么理解ES6中Decorator的?使用场景?
问题解析
这个问题考察对ES6装饰器(Decorator)的理解。装饰器是一种设计模式,用于在不修改原类和继承的情况下动态扩展对象功能。需要理解装饰器的本质、语法、执行顺序以及实际应用场景。
核心概念
1. 装饰器的本质
- 设计模式:装饰器模式的语法化实现
- 元编程:在编译阶段修改类和类成员的行为
- AOP思想:面向切面编程,分离横切关注点
2. 装饰器类型
- 类装饰器:修饰整个类,接收target参数
- 属性装饰器:修饰类属性,接收target、name、descriptor
- 方法装饰器:修饰类方法,接收target、name、descriptor
- 参数装饰器:修饰方法参数
3. 执行特性
- 编译时执行:在代码编译阶段执行,而非运行时
- 洋葱模型:从外到内进入,从内到外执行
- 不能用于函数:存在函数提升问题
详细解答
类装饰器
// 类装饰器接收一个参数:target(被装饰的类)
function logClass(target) {
console.log('类装饰器执行');
console.log('target:', target.name);
// 可以添加静态属性或方法
target.log = function() {
console.log(`This is ${target.name}`);
};
// 可以修改原型
target.prototype.createdAt = new Date();
}
@logClass
class MyClass {
constructor() {
this.name = 'instance';
}
}
MyClass.log(); // "This is MyClass"
const instance = new MyClass();
console.log(instance.createdAt); // 当前时间
// 带参数的类装饰器(装饰器工厂)
function logClassWithParams(message) {
return function(target) {
console.log(message);
target.prototype.message = message;
};
}
@logClassWithParams('Hello Decorator')
class AnotherClass {}
const another = new AnotherClass();
console.log(another.message); // "Hello Decorator"
类属性装饰器
// 属性装饰器接收三个参数:
// target - 类的原型对象(静态属性时为类本身)
// name - 属性名
// descriptor - 属性描述符(可选,取决于实现)
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
function logProperty(target, name) {
let value;
const getter = function() {
console.log(`Getting ${name}`);
return value;
};
const setter = function(newVal) {
console.log(`Setting ${name} = ${newVal}`);
value = newVal;
};
Object.defineProperty(target, name, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
name;
@readonly
id = '12345';
constructor(name) {
this.name = name;
}
}
const person = new Person('Tom');
// Setting name = Tom
console.log(person.name);
// Getting name
// "Tom"
// person.id = 'new'; // TypeError: Cannot assign to read only property
方法装饰器
// 方法装饰器接收三个参数:
// target - 类的原型对象
// name - 方法名
// descriptor - 方法描述符
function logMethod(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${name} with`, args);
const result = originalMethod.apply(this, args);
console.log(`${name} returned`, result);
return result;
};
return descriptor;
}
function measureTime(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${name} took ${end - start}ms`);
return result;
};
return descriptor;
}
class Calculator {
@logMethod
@measureTime
add(a, b) {
return a + b;
}
@logMethod
multiply(a, b) {
return a * b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Calling add with [2, 3]
// add took 0.1ms
// add returned 5
装饰器执行顺序(洋葱模型)
function decoratorA(target, name, descriptor) {
console.log('A enter');
const method = descriptor.value;
descriptor.value = function(...args) {
console.log('A before');
const result = method.apply(this, args);
console.log('A after');
return result;
};
console.log('A exit');
return descriptor;
}
function decoratorB(target, name, descriptor) {
console.log('B enter');
const method = descriptor.value;
descriptor.value = function(...args) {
console.log('B before');
const result = method.apply(this, args);
console.log('B after');
return result;
};
console.log('B exit');
return descriptor;
}
function decoratorC(target, name, descriptor) {
console.log('C enter');
const method = descriptor.value;
descriptor.value = function(...args) {
console.log('C before');
const result = method.apply(this, args);
console.log('C after');
return result;
};
console.log('C exit');
return descriptor;
}
class Example {
@decoratorA
@decoratorB
@decoratorC
method() {
console.log('Original method');
}
}
// 执行顺序:
// C enter -> C exit
// B enter -> B exit
// A enter -> A exit
// 调用时:A before -> B before -> C before -> Original -> C after -> B after -> A after
const ex = new Example();
ex.method();
不能用于函数
// 装饰器不能用于普通函数,因为函数存在变量提升
@logFunction // SyntaxError: Leading decorators must be attached to a class declaration
function myFunc() {}
// 原因:函数提升导致装饰器在函数定义之前执行
// 类不会提升,所以类装饰器没有问题
// 变通方案:使用高阶函数
function logFunction(fn) {
return function(...args) {
console.log(`Calling ${fn.name}`);
return fn.apply(this, args);
};
}
const loggedFunc = logFunction(function myFunc() {
console.log('executing');
});
深入理解
1. 装饰器的编译原理
装饰器在编译阶段被转换为函数调用:
// 源码
@decorator
class MyClass {
@methodDecorator
method() {}
}
// 编译后(近似)
let MyClass = decorator(class MyClass {
method() {}
});
Object.defineProperty(MyClass.prototype, 'method',
methodDecorator(MyClass.prototype, 'method',
Object.getOwnPropertyDescriptor(MyClass.prototype, 'method')
)
);
这种转换使得装饰器能够在类定义时执行,实现元编程能力。
2. 装饰器与元数据(Metadata)
// 配合reflect-metadata库实现元数据存储
import 'reflect-metadata';
const REQUIRED_KEY = 'required';
function required(target, propertyKey) {
const existing = Reflect.getMetadata(REQUIRED_KEY, target) || [];
Reflect.defineMetadata(REQUIRED_KEY, [...existing, propertyKey], target);
}
function validate(target) {
const requiredProps = Reflect.getMetadata(REQUIRED_KEY, target) || [];
for (const prop of requiredProps) {
if (target[prop] === undefined) {
throw new Error(`Property ${prop} is required`);
}
}
}
class User {
@required
name;
@required
email;
constructor(data) {
Object.assign(this, data);
validate(this);
}
}
// 这种机制是许多框架依赖注入的基础
3. 装饰器的AOP应用
// 面向切面编程:将横切关注点(日志、权限、缓存等)从业务逻辑分离
// 权限检查切面
function requireRole(role) {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
if (!this.user || this.user.role !== role) {
throw new Error(`Requires ${role} role`);
}
return original.apply(this, args);
};
return descriptor;
};
}
// 缓存切面
function cache(ttl = 60000) {
const cacheStore = new Map();
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
const key = JSON.stringify(args);
if (cacheStore.has(key)) {
const { value, timestamp } = cacheStore.get(key);
if (Date.now() - timestamp < ttl) {
return value;
}
}
const result = original.apply(this, args);
cacheStore.set(key, { value: result, timestamp: Date.now() });
return result;
};
return descriptor;
};
}
class AdminService {
user = { role: 'admin' };
@requireRole('admin')
@cache(5000)
getSensitiveData(id) {
console.log('Fetching from database...');
return { id, data: 'sensitive' };
}
}
4. 装饰器与依赖注入
// 简化的依赖注入实现
const container = new Map();
function injectable(target) {
container.set(target, new target());
return target;
}
function inject(token) {
return function(target, propertyKey) {
Object.defineProperty(target, propertyKey, {
get() {
return container.get(token);
}
});
};
}
@injectable
class Database {
query(sql) {
return `Result of ${sql}`;
}
}
@injectable
class UserService {
@inject(Database)
db;
getUsers() {
return this.db.query('SELECT * FROM users');
}
}
// 这种模式是Angular、NestJS等框架的核心
最佳实践
1. react-redux connect
// 装饰器使代码更简洁
import { connect } from 'react-redux';
// 不使用装饰器
const ConnectedComponent = connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
// 使用装饰器
@connect(mapStateToProps, mapDispatchToProps)
class MyComponent extends React.Component {
render() {
return <div>{this.props.data}</div>;
}
}
// 多个装饰器组合
@connect(mapStateToProps)
@withRouter
@withStyles(styles)
class EnhancedComponent extends React.Component {}
2. Mixins实现
// 使用装饰器实现Mixin
function mixins(...list) {
return function(target) {
Object.assign(target.prototype, ...list);
};
}
const Foo = {
foo() {
console.log('foo');
}
};
const Bar = {
bar() {
console.log('bar');
}
};
@mixins(Foo, Bar)
class MyClass {}
const obj = new MyClass();
obj.foo(); // "foo"
obj.bar(); // "bar"
3. 注解式编程
// 类似Java注解的风格
function api(config) {
return function(target, name, descriptor) {
descriptor.value.apiConfig = config;
return descriptor;
};
}
function validate(schema) {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(data) {
// 验证数据
if (!schema.validate(data)) {
throw new Error('Validation failed');
}
return original.call(this, data);
};
return descriptor;
};
}
class UserController {
@api({ method: 'GET', path: '/users/:id' })
@validate(userSchema)
getUser(id) {
// ...
}
@api({ method: 'POST', path: '/users' })
@validate(createUserSchema)
createUser(data) {
// ...
}
}
// 自动注册路由
const controller = new UserController();
Object.getOwnPropertyNames(UserController.prototype)
.forEach(key => {
const method = controller[key];
if (method.apiConfig) {
router[method.apiConfig.method.toLowerCase()](
method.apiConfig.path,
method.bind(controller)
);
}
});
4. 装饰器工厂模式
// 创建可配置的装饰器
function createDecorator(config) {
return function(target, name, descriptor) {
// 使用config配置装饰器行为
return descriptor;
};
}
// 日志级别装饰器
function log(level = 'info') {
const colors = {
info: '\x1b[36m',
warn: '\x1b[33m',
error: '\x1b[31m'
};
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`${colors[level]}[${level.toUpperCase()}] ${name}\x1b[0m`);
return original.apply(this, args);
};
return descriptor;
};
}
class Service {
@log('info')
fetchData() {}
@log('warn')
deprecatedMethod() {}
@log('error')
handleError() {}
}
面试要点
-
装饰器的本质:
- 装饰器模式的语法化
- 编译时执行的元编程
- AOP思想的实现
-
装饰器类型和参数:
- 类装饰器:接收target(类本身)
- 属性/方法装饰器:接收target、name、descriptor
- 参数装饰器:接收target、name、parameterIndex
-
执行顺序(洋葱模型):
- 多个装饰器时,从外到内进入,从内到外执行
- 类装饰器最后执行
- 方法装饰器先于类装饰器执行
-
不能用于函数的原因:
- 函数存在变量提升
- 装饰器在编译时执行,但函数提升后执行顺序混乱
- 类不会提升,所以类装饰器没有问题
-
使用场景:
- react-redux的connect
- Mixins实现
- 注解式编程(路由、API定义)
- AOP切面(日志、权限、缓存)
- 依赖注入
-
与TypeScript的关系:
- TypeScript实验性支持装饰器
- 需要开启
experimentalDecorators配置 - 可以配合
emitDecoratorMetadata使用反射元数据
-
注意事项:
- 装饰器提案仍在TC39流程中(Stage 3)
- Babel和TypeScript的实现可能略有差异
- 生产环境使用需要关注浏览器/Node版本支持