学习一下Ts5.0装饰器

本文介绍了TS5.0中的装饰器新语法,包括环境配置、装饰器概念、如何创建类方法装饰器、装饰器Creator以及装饰器抽象工厂。通过示例展示了装饰器如何用于日志记录、参数校验等场景,强调了装饰器在代码复用和逻辑分离上的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Ts5.0 装饰器新语法

ts5.0 发布了一些新特性,没有在官网的 handbook 上找到学习资料,在油管上找了一些视频来学习,讲得还挺不错, 下面主要来入门下 Method Decorator 的用法。

1. 环境准备

ts 版本修改

  1. 自己下载 ts5 版本,然后在 vscode 右下角选择 ts 版本为 workspace5.0.2
  2. 或者直接用 vscode 插件 JavaScript and TypeScript Nightly 里的最新版本
  3. 用 ts 官网的 playground 也行

全局下载 ts-node

这个插件可以直接运行 typescript 文件

提示:老装饰器语法的选项 experimentalDecorators 一定要关掉,否则走的老版本。

2. 装饰器介绍

装饰器语法是: @decoratorName, 主要作用就是在运行时通过包装一个新的函数,给被装饰的函数增加一些新的功能。

decoratorName 的格式是: (target: any, context: any) => any

  • target 是被装饰的目标对象
  • context 是上下文信息对象,这个比较厉害,可以通过 kind(被装饰的对象类型), name(被装饰的对象名字), private(是否是私有的), static(是否是静态的), access(访问器对象) 属性拿到被装饰对象的各种信息,还可以通过 addInitializer 在类被实例化或定义的时候注入额外的启动方法。

可以使用 高阶函数Object.defineProperty 来模拟生成, 所以兼容性很 ok。

3. 实现一个类方法装饰器

// demo.ts
// This 是实例类型
// Args 是参数类型
// Return 是返回值类型,可推断
// 装饰器返回的函数的类型在这三部分必须和原先函数上的保持一致,这样才能正确替换
function loggedMethod<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<
    This,
    (this: This, ...args: Args) => Return
  >
) {
  const methodName = String(context.name);

  const replaceMethod = function (this: This, ...args: Args) {
    console.log(`LOG: Entering method ${methodName}`);
    const result = target.call(this, ...args);
    console.log(`LOG: Exiting method ${methodName}`);

    return result;
  };

  return replaceMethod;
}

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @loggedMethod
  intro(guest: string) {
    console.log(`Hello ${guest}, my name is ${this.name}`);
  }
}

const p = new Person('jack');
p.intro('tom');

运行之后得到:

LOG: Entering method intro
Hello tom, my name is jack
LOG: Exiting method intro

可以看到,类方法 intro 被装饰成功了,它在原本的功能上新增了函数启动和退出的打印逻辑,这在实际应用中可以改造成入参校验逻辑 和 副作用清理逻辑。

4. 实现装饰器 Creator

如果装饰器的逻辑依赖一些动态的值,就可以实现一个装饰器 creator 来满足,它其实还是一个包装的函数工厂,可以根据传入的参数来返回特定功能的装饰器函数。

比如:在前面的功能基础上,让装饰器支持打印不同的日志类型


+ function log(level: 'INFO' | 'WARN' | 'ERROR') {
    return function loggedMethod<This, Args extends any[], Return>(
      target: (this: This, ...args: Args) => Return,
      context: ClassMethodDecoratorContext<
        This,
        (this: This, ...args: Args) => Return
      >
    ) {
      const methodName = String(context.name);

      const replaceMethod = function (this: This, ...args: Args) {
+       console.log(`${level}: Entering method ${methodName}`);
        const result = target.call(this, ...args);
+       console.log(`${level}: Exiting method ${methodName}`);

        return result;
      };

      return replaceMethod;
    };
+ }

调用的时候这样:

@log('INFO')
intro(guest: string) {
  console.log(`Hello ${guest}, my name is ${this.name}`);
}

得到的结果是:

INFO: Entering method intro
Hello tom, my name is jack
INFO: Exiting method intro

再比如:实现一个限制入参长度的装饰器 creator

function limit(count: number) {
  return function <This, Args extends [arg: any[], ...rest: any[]], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<
      This,
      (this: This, ...args: Args) => Return
    >
  ) {
    const methodName = String(context.name);
  
    const replaceMethod = function (this: This, ...args: Args) {
      if (args[0].length > count) {
        throw new Error(`${methodName}, cannot call with more than ${count} items`)
      }
  
      return target.call(this, ...args);
    };
  
    return replaceMethod;
  }
}


class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  // 只能向最多 2 个人介绍
  @limit(2)
  introGroup(guests: string[]) {
    guests.forEach((g) => {
      this.intro(g);
    });
  }

  @log('INFO')
  intro(guest: string) {
    console.log(`Hello ${guest}, my name is ${this.name}`);
  }
}

const p = new Person('jack');
p.introGroup(['tom', 'ben', 'chris']);

运行后的结果是:

throw new Error(`${methodName}, cannot call with more than ${count} items`)
              ^
Error: introGroup, cannot call with more than 2 items

像这样再拓展下去,其实可以实现非常多有用的装饰器。

5. 装饰器抽象工厂

继续拓展,如果现在需要制作一系列的负责校验的装饰器,比如:参数类型校验,参数长度校验,返回结果校验… 那么这一类的装饰器就会有很多的公共逻辑,可以继续抽象出一个专门产出校验类装饰器creator 的工厂。

// 抽象工厂函数
function makeGuardDecorator<T, A extends any[]>(
  fn: (t: T, ...args: A) => boolean,
  errMessage: string
) {
  return function (t: T) {
    return function <This, Args extends A, Return>(
      target: (this: This, ...args: Args) => Return,
      context: ClassMethodDecoratorContext<
        This,
        (this: This, ...args: Args) => Return
      >
    ) {
      const methodName = String(context.name);

      const replaceMethod = function (this: This, ...args: Args) {
        if (fn(t, ...args)) {
          return target.call(this, ...args);
        }
        // 如果校验失败
        throw new Error(`${methodName}: ${errMessage}, arg: ${t}`);
      };

      return replaceMethod;
    };
  };
}

// 生成装饰器 creator
const limit = makeGuardDecorator(
  (count: number, arg: any[], ...rest: any[]) => {
    return arg.length <= count;
  },
  `too many element in array`
);

const startsWith = makeGuardDecorator(
  (letter: string, arg: string, ...rest: any[]) => {
    return arg.charAt(0).toLowerCase() === letter.toLowerCase();
  },
  `starts with wrong letter`
);

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @limit(2)
  introGroup(guests: string[]) {
    guests.forEach((g) => {
      this.intro(g);
    });
  }

  @startsWith('T')
  intro(guest: string) {
    console.log(`Hello ${guest}, my name is ${this.name}`);
  }
}

const p = new Person('jack');
p.introGroup(['tom', 'ben', 'chris']);

运行后的结果是:

Hello tom, my name is jack
throw new Error(`${methodName}: ${errMessage}, arg: ${t}`);
              ^
Error: intro: starts with wrong letter, arg: T

可以看到,通过这种方式我们让装饰器功能更强大的同时,还聚合了很多公共的功能,这在编写某个具体功能的装饰器时,反而简单了,不需要写那么多的泛型和传导。

6. 多个装饰器

可以把多个装饰器组合起来形成更强大的多层级装饰器。
比如:我们希望 intro 方法具有打印进入和退出日志功能的同时,还能校验参数的首字符是否符合要求,就可以这样:


class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  @limit(3)
  introGroup(guests: string[]) {
    guests.forEach((g) => {
      this.intro(g);
    });
  }
  
  // 多个装饰器的装饰顺序:从下到上 (一层一层被包装和替换,下面在编译后的实现中可以看到)
  // 多个装饰器的调用顺序:从上到下
  @log('WARN')
  @startsWith('T')
  intro(guest: string) {
    console.log(`Hello ${guest}, my name is ${this.name}`);
  }
}

运行的结果是:

# ts-node .\demo5.ts
WARN: Entering method intro
Hello tom, my name is jack
WARN: Exiting method intro
WARN: Entering method intro
throw new Error(`${methodName}: ${errMessage}, arg: ${t}`);
              ^
Error: intro: starts with wrong letter, arg: T

可以看到,由于 @log('WARN')@startsWith('T') 的前面,所以会即使 startsWith判断失败,也会在之前先调用一次 log,但不是完整的调用,因为在代码上 target.call(this, args) 会报错然后退出程序。

如果把两个顺序换下,结果就不会多打印一次 WARN: Entering method intro

7.编译后的装饰器

把上面的例子复制到 ts playGround 中, 可以看到编译后的实现代码, 从中可以得到一些有用的信息:

  1. 装饰器的存储是一个闭包的形式,它是和属性是绑定在一起
  2. 在类完成定义的时候,装饰器就已经完成了初始化调用,后续对类方法的调用会直接使用替换好的函数
let Person = (() => {
    var _a;
    let _instanceExtraInitializers = [];
    // 使用闭包存储装饰器
    let _introGroup_decorators;
    let _intro_decorators;
    return _a = class Person {
            constructor(name) {
                // 执行 context 上添加的启动函数
                this.name = (__runInitializers(this, _instanceExtraInitializers), void 0);
                this.name = name;
            }
            introGroup(guests) {
                guests.forEach((g) => {
                    this.intro(g);
                });
            }
            intro(guest) {
                console.log(`Hello ${guest}, my name is ${this.name}`);
            }
        },
        (() => {
            // 装饰器使用 _propertyName_decorators 来命名,可以看成是一个栈结构(出口向下)
            _introGroup_decorators = [limit(3)];
            _intro_decorators = [log('WARN'), startsWith('T')];
            // 对指定的装饰对象应用装饰器
            __esDecorate(_a, null, _introGroup_decorators, { kind: "method", name: "introGroup", static: false, private: false, access: { has: obj => "introGroup" in obj, get: obj => obj.introGroup } }, null, _instanceExtraInitializers);
            __esDecorate(_a, null, _intro_decorators, { kind: "method", name: "intro", static: false, private: false, access: { has: obj => "intro" in obj, get: obj => obj.intro } }, null, _instanceExtraInitializers);
        })(),
        _a;
})();
  1. 装饰器应用是通过修改方法对应的 descriptor 并通过 Object.defineProperty完成替换的
  2. 从下面代码也可以看到替换顺序是从下到上的(栈顶向下),所以在前面会看到:多个装饰器的时候,上层的装饰器执行权更高。
var __esDecorate =
  (this && this.__esDecorate) ||
  function (
    ctor,
    descriptorIn,
    decorators,
    contextIn,
    initializers,
    extraInitializers
  ) {
    function accept(f) {
      if (f !== void 0 && typeof f !== 'function')
        throw new TypeError('Function expected');
      return f;
    }
  
    var kind = contextIn.kind,
      // accessor descriptor key 或者 data descriptor key
      key = kind === 'getter' ? 'get' : kind === 'setter' ? 'set' : 'value';
    // 静态的装饰 target 是类本身,否则是类的原型
    var target =
      !descriptorIn && ctor
        ? contextIn['static']
          ? ctor
          : ctor.prototype
        : null;
    // 取出被装饰器属性对应的属性描述符
    var descriptor =
      descriptorIn ||
      (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
    var _,
      done = false;
    // 从栈顶依次取出每个装饰器函数, 注册Initializer -> 调用函数 --> 替换原来的值
    for (var i = decorators.length - 1; i >= 0; i--) {
      var context = {};
      for (var p in contextIn) context[p] = p === 'access' ? {} : contextIn[p];
      for (var p in contextIn.access) context.access[p] = contextIn.access[p];
      context.addInitializer = function (f) {
        if (done)
          throw new TypeError(
            'Cannot add initializers after decoration has completed'
          );
        extraInitializers.push(accept(f || null));
      };
      // 调用装饰器函数,获取 replaceMethod
      var result = (0, decorators[i])(
        kind === 'accessor'
          ? { get: descriptor.get, set: descriptor.set }
          : descriptor[key],
        context
      );
      if (kind === 'accessor') {
        if (result === void 0) continue;
        if (result === null || typeof result !== 'object')
          throw new TypeError('Object expected');
        if ((_ = accept(result.get))) descriptor.get = _;
        if ((_ = accept(result.set))) descriptor.set = _;
        if ((_ = accept(result.init))) initializers.push(_);
      } else if ((_ = accept(result))) {
        if (kind === 'field') initializers.push(_);
        else descriptor[key] = _; // 修改属性描述符对应的 value 值
      }
    }
    // 使用 Object.defineProperty 完成替换
    // 如果是多个装饰器会被覆盖,但是逻辑上是层层包装的
    if (target) Object.defineProperty(target, contextIn.name, descriptor);
    done = true;
  };

结束

至此装饰器总算是不再那么神秘了。项目中如果遇到一些主逻辑以外的通用功能,使用上装饰器可以很方便的进行逻辑复用,并且让代码聚焦在主业务逻辑的处理上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值