一文搞懂 TypeScript 装饰器

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前段时间为了开发 HarmonyOS 路由框架,学习了 TypeScript 装饰器。在学习中发现无论是官方文档还是线上资料,鲜有文章能将装饰器简单通俗的表述清楚,故而萌生了撰写此文的想法。

当然,作为一个前端门外汉、TS初学者,写这篇文字更主要的还是为了加深自己对装饰器的理解。新手上路,难免有不足之处,还请各位看官轻喷。

一. 什么是装饰器?

官方文档上的解释是:

装饰器是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

这段官方解释给了几个信息:

  1. 装饰器用在类、属性、方法或者参数等之上;

    注:可修改、替换被装饰的目标,也可已在不影响原有特性的前提下增加附属功能。

  2. 使用@+函数(或者说表达式)的形式来书写;

  3. 这个函数在运行时间被调用,函数入参通常为与被装饰对象相关的一些信息;

  4. 这个函数要么不返回值,要么返回一个可以对象来替换被装饰的目标。(这一点是官方解释里未说明)

示例代码如下:

ts 复制代码
function MyDecorator(target: any) {
  ...
}

@MyDecorator
class User{
  ...
}

熟悉Java的同学,现在应该发现了,这不就是java中的注解吗!不同的是 TypeScript 装饰器只在运行时被调用,而 Java 中的注解能作用于源码阶段、编译阶段和运行时。

二. 装饰器的分类

装饰器的分类从写法上来区分,可分为:

markdown 复制代码
* 普通装饰器(不可带参数)
* 装饰器工厂(可以带参数)

从类别上来区分,TypeScript 中装饰器主要分为以下五类:

markdown 复制代码
* 类装饰器 ClassDecorator
* 属性装饰器 PropertyDecorator
* 方法装饰器 MethodDecorator
* 参数装饰器 ParammeterDecorator
* 访问符装饰器 AccessorDecorator

前四类装饰器较常用,后面的章节我们将分别介绍它们的定义以及实现方法、应用场景。

三. 装饰器的写法

这里用两段代码来分别演示普通装饰器和装饰器工厂的写法,他们的主要区别是:普通装饰器不可带参数,而装饰器工厂可以带参数。

3.1 普通装饰器

直接将函数作为装饰器函数,我们称之为普通装饰器。

ts 复制代码
/**
 * 类装饰器(普通装饰器写法)
 */
export function Route(target: object) {
  //TODO
  ...
}
  
//=========================================================
  
/**
 * 使用 Route 装饰器:
 */
@Route()
struct DetailPage {
  build() {
    ...
  }
}

3.2 装饰器工厂

装饰器工厂函数写法,可以在使用时传参,例如下面的 routePath 就是入参。

ts 复制代码
/**
 * 类装饰器(装饰器工厂实现)
 */
export function Route(routePath: string): ClassDecorator {
  return (target: object) => {
    //TODO
    ...
  }
}

//=========================================================
 
/**
 * 使用 Route 装饰器:
 */
@Route({ routePath: '/jump/entry/detail'})
struct DetailPage {
  build() {
    ...
  }
}

在装饰器工厂函数中,首先会执行Route() ,然后在使用返回的匿名函数作为装饰器的实际逻辑。

三. 装饰器分类

3.1 类装饰器 (ClassDecorator)

markdown 复制代码
类装饰器在类声明之前被声明(紧靠着类声明);类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。 

* 类装饰器不能用在声明文件中( `.d.ts`),也不能用在任何外部上下文中(比如`declare`的类)
* 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
* 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

简单来说,类装饰器是直接作用在类上的装饰器,类装饰器的类型描述如下:

ts 复制代码
declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

它的参数只有一个:

  • target(当前类本身)

示例代码:

ts 复制代码
function CustomClassDecorator(info: string): ClassDecorator {
  return (target: any) => {
    console.log(info + ',' + target.prototype)
  }
}

@CustomClassDecorator('你好')
class User {
  public name!: string

  constructor() {
    this.name = '马东锡'
  }
}

//输出日志:你好,马东锡

3.2 属性装饰器 (PropertyDecorator)

ts 复制代码
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。

* 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
* 属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。

属性装饰器可以定义在类的属性上,它的类型描述如下:

ts 复制代码
declare type PropertyDecorator = (
  targetClassPrototype: Object,
  propertyName: string | symbol
) => void;

它的参数有两个:

  • targetClassPrototype:类的原型
  • propertyName:属性名

示例代码:

ts 复制代码
function CustomPropertyDecorator(userName: string): PropertyDecorator {
  return (targetClassPrototype: any,
          propertyName: string | symbol) => {

    console.log(targetClassPrototype); // User: {}
    console.log(propertyName.toString()); // userName

    targetClassPrototype[propertyName] = userName
    targetClassPrototype['nickName'] = '男神'
  }
}

class User {
  @CustomPropertyDecorator('马东锡')
  public userName!: string

  constructor() {

  }
}

let user = new User()
console.log(user.userName) // 马东锡
console.log(user.nickName) // 男神

3.3 方法装饰器 (MethodDecorator)

markdown 复制代码
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的*属性描述符*上,可以用来监视,修改或者替换方法定义。

* 方法装饰器不能用在声明文件( `.d.ts`),重载或者任何外部上下文(比如`declare`的类)中;
* 方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符
* 如果方法装饰器返回一个值,它会被用作方法的属性描述符。

方法装饰器顾名思义就是定义在类中方法上的装饰器,他的类型描述如下:

ts 复制代码
declare type MethodDecorator = <T>(
  targetClassPrototype: Object,
  methodName: string | symbol,
  propertyDescriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

它接收三个参数:

  • targetClassPrototype:类的原型
  • methodName: 方法名
  • propertyDescriptor: 属性描述符

示例代码如下:

ts 复制代码
function CustomMethodDecorator(info: string): MethodDecorator {
  return (targetClassPrototype: any,
          methodName: any,
          propertyDescriptor: PropertyDescriptor) => {
    console.log(targetClassPrototype) // User: {}
    console.log(methodName) //sayHello

    let originMethod = propertyDescriptor.value

    propertyDescriptor.value = function (...args: any) {
      console.log("before")
      console.log("我" + info + "来了") //我马东锡来了
      originMethod.call(this, args)
      console.log("after")
    }
  }
}

class User {
  @CustomMethodDecorator('马东锡')
  sayHello() {
    console.log('执行sayHello()方法)')
  }
}

3.4 参数装饰器 ParammeterDecorator

markdown 复制代码
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 

* 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。
* 参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。
* 参数装饰器只能用来监视一个方法的参数是否被传入。
* 参数装饰器的返回值会被忽略。

参数装饰器定义在方法参数上,它的的类型描述如下:

ts 复制代码
declare type ParameterDecorator = (
  target: Object,
  methodName: string | symbol,
  parameterIndex: number
) => void;

参数装饰器接收三个参数:

  • targetClassPrototype:类原型
  • methodName
    • 如果参数修饰器所在的方法为类的静态/实例方法时,此参数为当前参数修饰器所在方法的方法名。
    • 如果参数修饰器所在的方法为类的构造函数参数修饰时,此参数为 undefined
  • parameterIndex:参数下标

示例代码:

ts 复制代码
function CustomParameterDecorator(tag: string): ParameterDecorator {
  return (targetClassPrototype: any,
          methodName: string | symbol,
          parameterIndex: number) => {

    console.log(tag); // 装饰实例方法的参数
    console.log(targetClassPrototype); // User: {}
    console.log(methodName.toString()); // sayHello
    console.log(parameterIndex.toString()); // 0

  }
}

class User {
  constructor() {

  }

  sayHello(@CustomParameterDecorator("装饰实例方法的参数") name: String) {
    console.log("你好," + name);
  }
}
相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒8 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端