返回首页

你是怎么理解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() {}
}

面试要点

  1. 装饰器的本质

    • 装饰器模式的语法化
    • 编译时执行的元编程
    • AOP思想的实现
  2. 装饰器类型和参数

    • 类装饰器:接收target(类本身)
    • 属性/方法装饰器:接收target、name、descriptor
    • 参数装饰器:接收target、name、parameterIndex
  3. 执行顺序(洋葱模型)

    • 多个装饰器时,从外到内进入,从内到外执行
    • 类装饰器最后执行
    • 方法装饰器先于类装饰器执行
  4. 不能用于函数的原因

    • 函数存在变量提升
    • 装饰器在编译时执行,但函数提升后执行顺序混乱
    • 类不会提升,所以类装饰器没有问题
  5. 使用场景

    • react-redux的connect
    • Mixins实现
    • 注解式编程(路由、API定义)
    • AOP切面(日志、权限、缓存)
    • 依赖注入
  6. 与TypeScript的关系

    • TypeScript实验性支持装饰器
    • 需要开启experimentalDecorators配置
    • 可以配合emitDecoratorMetadata使用反射元数据
  7. 注意事项

    • 装饰器提案仍在TC39流程中(Stage 3)
    • Babel和TypeScript的实现可能略有差异
    • 生产环境使用需要关注浏览器/Node版本支持