TypeScript装饰器与设计模式

简介: TypeScript装饰器与设计模式

TypeScript装饰器与设计模式

什么是装饰器

在内部,装饰器只是一个函数。在 js 执行时,将目标(类,函数,属性等)传入装饰器,并执行。

首先装饰器是 js/ts 的一个实验属性,我们需要在 tsconfig.json 中找到"experimentalDecorators": true,并打开它。

类装饰器

首先装饰器的函数名我们通常情况下使用大驼峰命名法,然后函数的参数类型取决于这个装饰器作用于哪里。比如这是个类装饰器,参数的类型就为Function,它是作用于构造函数的。这里的参数也是指的传入的类的构造函数。

在使用类装饰器的时候将其写在类的上面(注意没有括号)

function Component(constructor: Function): void {
  console.log("Component decorator called");
  constructor.prototype.uniqueId = Date.now();
  constructor.prototype.inserInDOM = () => {
    console.log("Inserting the component in the DOM");
  };
}

@Component
class ProfileComponent {}
//Component decorator called

装饰器工厂

现在我们想要创建参数化的装饰器

// 装饰器工厂/Decorator factory
function Component(value: number) {
  return (constructor: Function) => {
    console.log("Component decorator called");
    constructor.prototype.options = value;
    constructor.prototype.uniqueId = Date.now();
    constructor.prototype.inserInDOM = () => {
      console.log("Inserting the component in the DOM");
    };
  };
}

@Component(1) //Component decorator called number 1
class ProfileComponent {}

这看起来就像一个工厂,用于创建装饰器。这样的函数叫做装饰器工厂。

我们现在让参数为对象:

type ComponentOptions = {
  selector: string;
};

// 装饰器工厂/Decorator factory
function Component(options: ComponentOptions) {
  return (constructor: Function) => {
    console.log("Component decorator called");
    constructor.prototype.options = options;
    constructor.prototype.uniqueId = Date.now();
    constructor.prototype.inserInDOM = () => {
      console.log(
        "Inserting the component in the DOM:" +
          constructor.prototype.options.selector
      );
    };
  };
}

@Component({ selector: "#my-profile" })
class ProfileComponent {
  inserInDOM() {} //需要声明
}

let profileComponent = new ProfileComponent();
profileComponent.inserInDOM(); //Inserting the component in the DOM:#my-profile

使用多个装饰器

我们可以同时使用多个装饰器

type ComponentOptions = {
  selector: string;
};

// 装饰器工厂/Decorator factory
function Component(options: ComponentOptions) {
  return (constructor: Function) => {
    console.log("Component decorator called");
    constructor.prototype.options = options;
    constructor.prototype.uniqueId = Date.now();
    constructor.prototype.inserInDOM = () => {
      console.log(
        "Inserting the component in the DOM:" +
          constructor.prototype.options.selector
      );
    };
  };
}

function Pipe(constructor: Function) {
  console.log("Pipe decorator called");
  constructor.prototype.pipe = true;
}

@Component({ selector: "#my-profile" })
@Pipe
class ProfileComponent {
  inserInDOM() {}
}
// Pipe decorator called
// Component decorator called

需要注意的是,我们的装饰器是按照相反的顺序应用的。

这背后的想法来自数学:在数学中如果我们有f(g(x))这样的表达式,然后我们会先求得 g(x)的值然后把它传给 f(x)。

方法装饰器

方法装饰器有三个参数:

参数 说明
参数一 普通方法是构造函数的原型对象 Prototype,静态方法是构造函数
参数二 方法名称
参数三 属性特征

我们这里不会用到 target 和 methodName,但由于我们又在 tsconfig.josn 中进行了配置,我们可以关闭这个配置,也可以使用带下划线_的前缀来忽略这个报错。

function Log(
  _target: any,
  _methodName: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value as Function;
  descriptor.value = function () {
    console.log("Before");
    original.call(this, "传入数据");
    console.log("After");
  };
}

class Person {
  @Log
  say(message: string) {
    console.log("Person says " + message);
  }
}

let person = new Person();
person.say("dom");
// Before
// Person says 传入数据
// After

我们发现 target 的类型使用的 any,虽然我们建议尽量不用 any,但也不是完全不用,我们在这里并不知道 target 的类型。method 的类型为 string,descriptor 属性特征的类型为 PropertyDescriptor。

我们在覆盖descriptor.value前将原方法保留并在新方法中调用。

我们发现我们在实例对象传的参数会被忽略。因为在我们的新方法中没有参数,而是直接调用的保存好的原函数original

如果我们像不被覆盖,我们可以这样写:

function Log(
  _target: any,
  _methodName: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value as Function;
  descriptor.value = function (message: string) {
    console.log("Before");
    original.call(this, message);
    console.log("After");
  };
}

class Person {
  @Log
  say(message: string) {
    console.log("Person says " + message);
  }
}

let person = new Person();
person.say("dom");
// Before
// Person says dom
// After

为了让这个装饰器能够在多个方法中使用,我们可以这样做:

function Log(
  _target: any,
  _methodName: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value as Function;
  descriptor.value = function (...args: any) {
    console.log("Before");
    original.call(this, ...args);
    console.log("After");
  };
}

值得注意的是:我们在写新方法时应该用函数表达式声明,而不是箭头函数声明。因为箭头函数没有自己的 this,他们的 this 指向当前实例对象

在访问器中使用装饰器

在 getter 和 setter 访问器中我们应该如何使用装饰器呢?

访问器与方法类似,所以我们和使用方法装饰器的时候一样,唯一不同的是,访问器不能使用descriptorvalue属性,在使用 get 访问器的时候我们要使用get属性。而不是value

function Capitalize(
  _target: any,
  _methodName: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.get;
  descriptor.get = function () {
    const result = original?.call(this);
    return typeof result === "string" ? result.toUpperCase() : result;
  };
}

class Person {
  constructor(public firstName: string, public lastName: string) {}

  @Capitalize
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

let person = new Person("Kevin", "Qian");
console.log(person.fullName); //KEVIN QIAN

属性装饰器

这里我们使用了装饰器工厂

属性装饰器的参数为:

参数 说明
参数一 普通方法是构造函数的原型对象 Prototype,静态方法是构造函数
参数二 属性名称
function MinLength(length: number) {
  return (target: any, propertyName: string) => {
    let value: string; // 注意要先声明

    const descriptor: PropertyDescriptor = {
      get() {
        return value;
      },
      set(newValue: string) {
        if (newValue.length < length)
          throw new Error(`${propertyName} should be at least ${length}`);
        value = newValue;
      },
    };

    Object.defineProperty(target, propertyName, descriptor); // 我们通过这个方法来改变我们的原属性。
  };
}

class User {
  @MinLength(4)
  password: string;
  constructor(password: string) {
    this.password = password;
  }
}

let user = new User("5678");
console.log(user.password); //5678

// user.password = "22"; //Error: password should be at least 4

// let errUser = new User("111"); //Error: password should be at least 4

我们可以看到,传入构造函数的值如果长度小于 4,则会报错。并且在重新赋值属性的时候,装饰器会被重新调用。进行一个验证,当长度小于 4 的时候报错。

参数装饰器

我们并不常使用参数装饰器,但如果你正在设计一个框架供其他人使用,你可能会用到参数装饰器。

我们通常情况将其用于存储这些参数的一些元数据

type WatchedParameter = {
  methodName: string;
  parameterIndex: number;
};

const watchedParameters: WatchedParameter[] = [];

function Watch(_target: any, methodName: string, parameterIndex: number) {
  watchedParameters.push({
    methodName,
    parameterIndex,
  });
}

class Vehicle {
  move(@Watch _speed: number) {}
}

console.log(watchedParameters); //[ { methodName: 'move', parameterIndex: 0 } ]
相关文章
|
4月前
|
JavaScript 前端开发 数据安全/隐私保护
TypeScript中装饰器的概念与使用场景
【4月更文挑战第23天】TypeScript的装饰器是特殊声明,用于附加到类的声明、方法、属性或参数,以修改行为。它们以`@expression`形式,其中`expression`是运行时调用的函数。装饰器应用场景包括:日志记录、调试、依赖注入、权限控制和AOP。通过装饰器,可以实现动态行为修改,如添加日志、注入依赖、控制权限以及事务管理。然而,应谨慎使用,避免过度复杂化代码。装饰器在现代 TypeScript 开发中扮演重要角色,帮助编写更健壮、可维护的代码。
|
4月前
|
存储 JavaScript 前端开发
TypeScript 5.2 beta 浅析:新的关键字 using 与新版装饰器元数据
TypeScript 5.2 beta 浅析:新的关键字 using 与新版装饰器元数据
|
22天前
|
设计模式 Java
重构你的代码:探索Java中的混合、装饰器与组合设计模式
【8月更文挑战第30天】在软件开发中,设计模式为特定问题提供了结构化的解决方案,使代码更易理解、维护及扩展。本文将介绍三种常用的 Java 设计模式:混合模式、装饰器模式与组合模式,并附有示例代码展示实际应用。混合模式允许通过继承多个接口或抽象类实现多重继承;装饰器模式可在不改变对象结构的情况下动态添加新功能;组合模式则通过树形结构表示部分-整体层次,确保客户端处理单个对象与组合对象时具有一致性。
15 1
|
2月前
|
JavaScript 前端开发 索引
TypeScript(八)装饰器
TypeScript(八)装饰器
22 0
|
4月前
|
设计模式
设计模式之装饰器 Decorator
设计模式之装饰器 Decorator
39 1
|
3月前
|
JavaScript 监控 编译器
29.【TypeScript 教程】装饰器(Decorator)
29.【TypeScript 教程】装饰器(Decorator)
33 0
|
4月前
|
设计模式 Java
Java 设计模式:混合、装饰器与组合的编程实践
【4月更文挑战第27天】在面向对象编程中,混合(Mixins)、装饰器(Decorators)和组合(Composition)是三种强大的设计模式,用于增强和扩展类的功能。
62 1
|
4月前
|
设计模式 算法 Java
【设计模式】美团三面:你连装饰器都举不出例子?
【设计模式】美团三面:你连装饰器都举不出例子?
36 0
|
4月前
|
缓存 JavaScript 前端开发
【TypeScript技术专栏】TypeScript中的装饰器与元编程
【4月更文挑战第30天】TypeScript的装饰器是元编程工具,用于修改类、方法等行为。它们允许实现日志、权限控制、缓存等功能,支持类装饰器、方法装饰器等多种类型。装饰器借助JavaScript的Proxy和Reflection API实现,但过度使用可能造成复杂性。正确运用能提升代码质量,但需注意类型安全和维护性。
74 0
|
4月前
react+typescript装饰器写法报错的解决办法
react+typescript装饰器写法报错的解决办法
63 1