JavaScript内核与高级编程之:`JavaScript` 的 `Decorators`:如何编写一个函数或类的 `Decorator`。

各位靓仔靓女,早上好! 很高兴今天能和大家聊聊JavaScript里一个挺有意思的特性:Decorators(装饰器)。 别害怕,听起来好像很高大上,但其实它就是个语法糖,让你的代码更优雅、更易读。 今天咱们就来深入浅出地扒一扒它,看看如何编写函数和类的装饰器。

开场白:为什么要用装饰器?

想象一下,你有一个函数,需要在执行前后做一些额外的事情,比如记录日志、验证权限、缓存结果等等。 你可能会这样做:

function myFunction() {
  console.log("函数开始执行..."); // 记录日志
  // ... 函数的核心逻辑 ...
  console.log("函数执行完毕..."); // 记录日志
}

如果有很多函数都需要做类似的事情,那你就要在每个函数里都写一遍这些额外的逻辑。 这不仅重复劳动,而且让代码变得臃肿不堪,难以维护。

这时候,装饰器就派上用场了! 它可以让你把这些额外的逻辑抽离出来,像给函数“穿衣服”一样,动态地给函数添加功能,而不用修改函数本身的逻辑。

什么是装饰器?

简单来说,装饰器就是一个函数,它接收一个函数或类作为参数,并返回一个新的函数或类,这个新的函数或类通常会在原有的函数或类上添加一些额外的功能。

装饰器的语法

在JavaScript里,装饰器使用 @ 符号来标记。 例如:

@log
function myFunction() {
  // ...
}

这里的 @log 就是一个装饰器,它会作用于 myFunction 函数。

编写函数装饰器

函数装饰器接收一个函数作为参数,并返回一个新的函数。 新的函数通常会在调用原始函数之前或之后执行一些额外的逻辑。

我们来编写一个简单的日志记录装饰器:

function log(target, name, descriptor) {
  const original = descriptor.value;

  if (typeof original !== 'function') {
    throw new TypeError('只能装饰函数');
  }

  descriptor.value = function (...args) {
    console.log(`调用函数 ${name},参数:`, args);
    const result = original.apply(this, args);
    console.log(`函数 ${name} 返回值:`, result);
    return result;
  };

  return descriptor;
}

class MyClass {
  @log
  add(a, b) {
    return a + b;
  }
}

const myInstance = new MyClass();
const sum = myInstance.add(1, 2);
console.log("最终结果:", sum);

上面的代码中,log 就是一个函数装饰器。 它接收三个参数:

  • target: 被装饰的类本身(如果是类方法)或者 undefined (如果是独立的函数)。
  • name: 被装饰的方法或属性的名称。
  • descriptor: 属性描述符,包含 value (函数本身), writable, enumerable, configurable 等属性。

装饰器内部首先获取到原始函数 original,然后创建一个新的函数,这个新的函数会在调用原始函数之前和之后记录日志。 最后,将新的函数赋值给 descriptor.value,并返回 descriptor

当我们调用 myInstance.add(1, 2) 时,实际上调用的是被装饰器修改后的函数,它会先记录日志,然后调用原始的 add 函数,最后再记录返回值日志。

更通用一点的装饰器

上面的装饰器只适用于简单的日志记录。 如果我们需要更灵活的装饰器,可以接受参数,例如,指定日志的前缀:

function logWithPrefix(prefix) {
  return function (target, name, descriptor) {
    const original = descriptor.value;

    if (typeof original !== 'function') {
      throw new TypeError('只能装饰函数');
    }

    descriptor.value = function (...args) {
      console.log(`${prefix} - 调用函数 ${name},参数:`, args);
      const result = original.apply(this, args);
      console.log(`${prefix} - 函数 ${name} 返回值:`, result);
      return result;
    };

    return descriptor;
  };
}

class MyClass {
  @logWithPrefix("DEBUG")
  add(a, b) {
    return a + b;
  }
}

const myInstance = new MyClass();
const sum = myInstance.add(1, 2);
console.log("最终结果:", sum);

logWithPrefix 函数返回一个装饰器函数,这个装饰器函数接收 target, name, descriptor 作为参数,并执行类似上面的日志记录操作,只不过在日志信息中添加了指定的前缀。

编写类装饰器

类装饰器接收一个类作为参数,并返回一个新的类。 新的类通常会在原有的类上添加一些新的属性或方法,或者修改原有的属性或方法。

我们来编写一个简单的类装饰器,给类添加一个 createdAt 属性,记录类的创建时间:

function createdAt(constructor) {
  return class extends constructor {
    constructor(...args) {
      super(...args);
      this.createdAt = new Date();
    }
  };
}

@createdAt
class MyClass {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const myInstance = new MyClass("Alice");
myInstance.sayHello();
console.log("创建时间:", myInstance.createdAt);

上面的代码中,createdAt 就是一个类装饰器。 它接收一个类 constructor 作为参数,并返回一个新的类,这个新的类继承自原始的类,并在构造函数中添加了 createdAt 属性。

类装饰器的一些高级用法

  • 修改类的原型

    类装饰器可以修改类的原型,添加新的方法或属性。 例如,我们可以给类添加一个 toString 方法:

    function addToString(constructor) {
      constructor.prototype.toString = function () {
        return `[object ${constructor.name}]`;
      };
    }
    
    @addToString
    class MyClass {
      constructor(name) {
        this.name = name;
      }
    }
    
    const myInstance = new MyClass("Bob");
    console.log(myInstance.toString());
  • 替换整个类

    类装饰器可以返回一个完全不同的类,替换掉原始的类。 这通常用于 mock 测试,或者根据不同的环境选择不同的实现。

    function mockClass(constructor) {
      return class MockClass {
        constructor() {
          console.log("使用 MockClass!");
        }
    
        mockMethod() {
          console.log("这是 Mock 方法!");
        }
      };
    }
    
    @mockClass
    class MyClass {
      constructor() {
        console.log("真正的 MyClass!");
      }
    
      realMethod() {
        console.log("这是 Real 方法!");
      }
    }
    
    const myInstance = new MyClass();
    // myInstance.realMethod(); // 报错,因为 MyClass 被替换了
    myInstance.mockMethod();

装饰器的应用场景

装饰器在实际开发中有很多应用场景,例如:

  • 日志记录: 记录函数或方法的调用信息,方便调试和排错。
  • 权限验证: 验证用户是否有权限访问某个函数或方法。
  • 缓存: 缓存函数的计算结果,避免重复计算。
  • 性能监控: 统计函数的执行时间,分析性能瓶颈。
  • 依赖注入: 将依赖注入到类中,实现解耦。
  • 数据验证: 验证数据的格式是否正确。

装饰器的一些注意事项

  • 装饰器的执行顺序: 如果一个函数或类被多个装饰器装饰,装饰器的执行顺序是从下往上,从里往外。
  • 装饰器的参数: 装饰器可以接收参数,也可以不接收参数。 接收参数的装饰器需要返回一个函数,这个函数才是真正的装饰器。
  • 装饰器的兼容性: JavaScript 的装饰器语法目前还是一个提案,需要使用 Babel 等工具进行转译才能在浏览器中运行。 不过,TypeScript 原生支持装饰器语法。

一些常用的表格

装饰器类型 接收参数 返回值 作用
类装饰器 类构造函数 新的类/构造函数 增强或替换整个类。可以修改类的原型、添加新的方法/属性,甚至完全替换这个类。
方法装饰器 target, name, descriptor descriptor 增强类的某个方法。 可以修改方法的行为,例如添加日志、权限验证等。descriptor 包含了方法的各种属性,如 value (方法本身)、writable、enumerable、configurable。 修改 descriptor.value 来修改方法的实现。
属性装饰器 target, name descriptor 增强类的某个属性。 通常用于对属性进行一些预处理或验证。 target 在类装饰器中是类的构造函数,在实例装饰器中是类的原型对象。 name 是属性名。 descriptor 包含了属性的配置信息,例如 get、set 方法。
参数装饰器 target, name, index void 增强方法的某个参数。 target 是类构造函数或类的原型对象(取决于是否是静态方法)。 name 是方法名。 index 是参数的索引。 参数装饰器通常用于记录参数信息,或者进行一些参数验证。 由于无法直接修改参数本身,参数装饰器更多的是用于元数据管理,例如配合反射 API 来实现依赖注入。

总结

装饰器是JavaScript里一个强大的特性,它可以让你以一种声明式的方式给函数和类添加额外的功能,提高代码的可读性和可维护性。 虽然目前还需要使用 Babel 等工具进行转译,但相信随着 JavaScript 语言的不断发展,装饰器会越来越普及。

希望今天的讲解对大家有所帮助。 以后写代码的时候,不妨试试装饰器,让你的代码更上一层楼! 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注