JS装饰器

536 阅读3分钟

从零开始理解 JavaScript 装饰器

装饰器(Decorator)是一种特殊的 JavaScript 语法,它允许你通过一种声明式的方式修改或增强类、类方法、类属性等的行为。就像它的名字"装饰"一样,它能为现有的代码添加额外的功能,而不需要直接修改原始代码。

1. 装饰器是什么?

想象你有一个礼物盒子(类或方法),装饰器就像是在这个盒子上系上丝带、贴上标签或加上其他装饰物,让盒子变得更漂亮或功能更多,但盒子本身并没有被改变。

2. 基本语法

装饰器使用 @ 符号开头,放在要装饰的目标前面:

@decorator
class MyClass {
  @decorator
  myMethod() {}
  
  @decorator
  myProperty = 'value';
}

3、装饰器的5种类型

3.1 类装饰器 - 装饰整个类

// 定义一个简单的类装饰器
function addAuthor(author) {
  return function(target) { // 这是一个装饰器工厂
    target.author = author; // 为类添加静态属性
    return target;
  }
}

@addAuthor('John Doe')
class Book {
  // 类内容
}

console.log(Book.author); // 输出: "John Doe"

转译后的代码

// 原始的装饰器工厂函数保持不变
function addAuthor(author) {
  return function(target) {
    target.author = author; // 为类添加静态属性
    return target;
  };
}

// 定义Book类
class Book {
  // 类内容
}

// 手动应用装饰器(替代 @addAuthor('John Doe') 语法)
const authorDecorator = addAuthor('John Doe');
const DecoratedBook = authorDecorator(Book);

// 测试
console.log(Book.author); // 输出: "John Doe"
console.log(DecoratedBook.author); // 输出: "John Doe"

3.2、方法装饰器 - 装饰类的方法

// 定义一个记录方法执行时间的装饰器
function logTime(target, name, descriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args) {
    console.time(name);
    const result = originalMethod.apply(this, args);
    console.timeEnd(name);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  @logTime
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3); // 控制台会输出执行时间

相当于下述代码

// 原始的装饰器函数保持不变
function logTime(target, name, descriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args) {
    console.time(name);
    const result = originalMethod.apply(this, args);
    console.timeEnd(name);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  add(a, b) {
    return a + b;
  }
}

// 手动应用装饰器(替代 @logTime 语法)
const addDescriptor = Object.getOwnPropertyDescriptor(Calculator.prototype, 'add');
const decoratedAddDescriptor = logTime(Calculator.prototype, 'add', addDescriptor);
Object.defineProperty(Calculator.prototype, 'add', decoratedAddDescriptor);

const calc = new Calculator();
calc.add(2, 3); // 控制台会输出执行时间

3.3、 属性装饰器 - 装饰类的属性

// 定义一个使属性只读的装饰器
function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Person {
  @readonly
  name = 'Alice';
}

const person = new Person();
person.name = 'Bob'; // 这里会报错,因为name是只读的

3.4、访问器装饰器 - 装饰getter/setter

// 定义一个缓存结果的装饰器
function cache(target, name, descriptor) {
  const getter = descriptor.get;
  let cachedValue;
  
  descriptor.get = function() {
    if (cachedValue === undefined) {
      cachedValue = getter.call(this);
    }
    return cachedValue;
  };
  
  return descriptor;
}

class ExpensiveComputation {
  @cache
  get result() {
    console.log('计算中...');
    return 42; // 假设这是个耗时的计算
  }
}

const instance = new ExpensiveComputation();
console.log(instance.result); // 第一次会计算
console.log(instance.result); // 第二次直接从缓存读取

3.5、参数装饰器 - 装饰方法的参数

// 定义一个检查参数类型的装饰器
function checkType(type) {
  return function(target, name, index) {
    // 存储参数类型信息
    target.__paramTypes = target.__paramTypes || {};
    target.__paramTypes[name] = target.__paramTypes[name] || [];
    target.__paramTypes[name][index] = type;
  };
}

class MathOperations {
  add(
    @checkType('number') a,
    @checkType('number') b
  ) {
    return a + b;
  }
}

// 实际类型检查需要在方法调用时实现

4、装饰器的执行顺序

装饰器的执行总是从上到下,从外到内的

  • 参数装饰器)(先执行)
  • 方法,属性,访问器装饰器
  • 类装饰器(最后执行)

5、为什么要用装饰器?

  • 代码更干净:将横切关注点(如日志,验证)与业务逻辑分开
  • 复用性高:可以轻松在多个地方应用相同的功能
  • 声明式编程:通过简单的注释就能添加复杂功能
  • 可组合性:可以添加多个装饰器

6、实际应用场景

  • 日志记录
  • 性能监控
  • 验证
  • 依赖注入
  • 权限控制
  • 数据绑定