javascript装饰器进入stage3了
flytam Lv4

前言

image

在3月底,js的装饰器提案终于进入了stage3,同时其metadata部分单独拆开仍处于stage2阶段详见。但是此装饰器却非平时我们广泛使用的装饰器。通过本文我们将了解下该js提案下装饰器的用法并对比和先前装饰器提案下用法的区别

decorator提案的历史

babel-plugin-proposal-decorators的文档我们可以看到,decorator提案之前主要经历了3个阶段+目前进入stage3阶段移除metadata的版本共4个版本。下文分别简称(legacy、2018-09、2021-12、stage3)

image
  • legacy

    stage 1阶段的提案,也是目前广为使用的用法,也基本等同于Typescript中开启experimentalDecorators的用法

  • 2018-09

    在2018.9进入stage2阶段后的提案,此时用法已经完全和stage1不一样

  • 2021-12

    2021.12针对此前的stage2提案又做了一次修改,用法又做了一点小修改

  • stage3

    最近正式进入stage3的提案。对比2021-12只是去掉了metadata部分,提案本身没有太大的改变。不出意外这也是以后作为标准的装饰器

详细用法

legacy

我们广为使用的用法。即Typescript中tsconfig中配置experimentalDecorators:true

1
2
3
4
5
{
"compileOptions": {
"experimentalDecorators": true
}
}

或者@babel/plugin-proposal-decorators配置

legacy: true。注意:最新的@babel/plugin-proposal-decorators已经将该配置迁移到version字段,即version: legacy

legacy下的装饰器更具体用法可以参考此前写的一篇文章2020的最后一天,不妨了解下装饰器

装饰器函数的签名主要如下

  • 类装饰器 (Class Decorators)
    类装饰器作用于类的构造函数,可用于修改或者替换一个 class 定义。
    一个装饰器函数签名如下:

    1
    type decorator = (target: Function) => Function | void;

    它接收被装饰的 class 作为target函数的参数,如果装饰器函数有返回值,则使用这个返回值作为新的 class

  • 属性装饰器 (Property Decorators)
    1、第一个参数。如果装饰的是静态方法,则是这个类 Target 本身;如果装饰的是原型方法,则是类的原型对象 Target.prototype

    2、第二个参数。这个属性的名称

    1
    2
    3
    4
    type decorator = (
    target: Target | Target.prototype,
    propertyKey: string
    ) => void;
  • 方法装饰器 (Method Decorators) + 访问器装饰器 (Accessor Decorators)
    1、第一个参数。如果装饰的是静态方法,则是这个类Target本身;如果装饰的是原型方法,则是类的原型对象Target.prototype

    2、第二个参数。这个方法的名称

    3、第三个参数,这个方法的属性描述符,通过descriptor.value可以直接拿到这个方法

    如果属性装饰器有返回值,这个返回值讲作为这个方法的属性描述符。对象的属性描述符就是调用Reflect.getOwnPropertyDescriptor(target, propertyKey)的返回值

1
2
3
4
5
type decorator = (
target: Target | Target.prototype,
propertyKey: string,
descriptor: PropertyDescriptor
) => Function | void;
  • 参数装饰器 (Parameter Decorators)
    1、第一个参数。如果装饰的是静态方法的参数,则是这个类Target本身;如果装饰的是原型方法的参数,则是类的原型对象Target.prototype

    2、第二个参数。参数所处的函数名称

    3、第三个参数,该参数位于函数参数列表的位置下标(number)

    1
    2
    3
    4
    5
    type decorator = (
    target: Target | Target.prototype,
    propertyKey: string,
    parameterIndex: number
    ) => void;
stage3

本次进入stage3提案的用法

装饰器函数签名如下:

1
2
3
4
5
6
7
8
9
10
11
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
isPrivate?: boolean;
isStatic?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;

装饰器函数包含两个入参参数

1、被装饰的值本身

2、被装饰值的上下文信息

  • kind :"class"|"method"|"getter"|"setter"|"field"|"accessor"。表示装饰器的类型
  • name 装饰值的名称
  • access 同个该属性读写值
  • isStatic 是否静态属性
  • isPrivate 是否私有属性
  • addInitializer 用于执行一些初始化逻辑

各种不同类型的装饰器如下

  • 方法装饰器

    1
    2
    3
    4
    5
    6
    7
    8
    type ClassMethodDecorator = (value: Function, context: {
    kind: "method";
    name: string | symbol;
    access: { get(): unknown };
    isStatic: boolean;
    isPrivate: boolean;
    addInitializer(initializer: () => void): void;
    }) => Function | void;

    方法装饰器接收被装饰的方法作为第一个参数并可选的返回一个函数。返回的函数将替代原先的函数。方法装饰器可作用于静态方法或者原型方法

  • 访问器装饰器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type ClassGetterDecorator = (value: Function, context: {
    kind: "getter";
    name: string | symbol;
    access: { get(): unknown };
    isStatic: boolean;
    isPrivate: boolean;
    addInitializer(initializer: () => void): void;
    }) => Function | void;

    type ClassSetterDecorator = (value: Function, context: {
    kind: "setter";
    name: string | symbol;
    access: { set(value: unknown): void };
    isStatic: boolean;
    isPrivate: boolean;
    addInitializer(initializer: () => void): void;
    }) => Function | void;

    访问器装饰器和方法装饰器类似,接收被装饰器的原始方法,可以返回一个函数替代原始方法

  • 属性装饰器

    1
    2
    3
    4
    5
    6
    7
    type ClassFieldDecorator = (value: undefined, context: {
    kind: "field";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    isStatic: boolean;
    isPrivate: boolean;
    }) => (initialValue: unknown) => unknown | void;

    和访问器装饰器、方法装饰器区别,属性装饰器的第一个参数为undefined。属性装饰器可以返回一个初始化函数,返回的初始化函数的入参为原始属性值,返回值为替代原始的属性值

  • 类装饰器

    1
    2
    3
    4
    5
    type ClassDecorator = (value: Function, context: {
    kind: "class";
    name: string | undefined;
    addInitializer(initializer: () => void): void;
    }) => Function | void;

    类装饰器第一个参数为被装饰的类,可以返回一个新的类去替代原有的类

此外stage3对比legacy提案特有的两个用法

  • 类自动访问器(Class Auto-Accessors)
    类自动访问器是一种新定义的行为。通过在类属性名前加上accessor关键字进行使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class C {
    accessor x = 1;
    }
    // 与常规字段不同,自动访问器在类原型上定义了 getter 和 setter。getter 和 setter 默认获取和设置私有字段上的值
    // 等同如下代码
    class C {
    #x = 1;

    get x() {
    return this.#x;
    }

    set x(val) {
    this.#x = val;
    }
    }

    类自动访问器也可以被装饰,装饰器函数签名如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type ClassAutoAccessorDecorator = (
    value: {
    get: () => unknown;
    set(value: unknown) => void;
    },
    context: {
    kind: "accessor";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    isStatic: boolean;
    isPrivate: boolean;
    addInitializer(initializer: () => void): void;
    }
    ) => {
    get?: () => unknown;
    set?: (value: unknown) => void;
    initialize?: (initialValue: unknown) => unknown;
    } | void;

    和普通的属性访问器略有区别,装饰器函数第一个参数的入参是一个包含原型上get/set函数的对象。可以通过返回一个包含get/set函数的对象来取代被装饰值的行为。也可以通过initialize函数修改初始值

  • addInitializer初始化逻辑函数
    stage3的装饰器函数中context参数入参会有一个addInitializer函数,它可以接收一个回调,用于执行一些初始化逻辑。一个实际的用法是例如注册web component。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    function customElement(name) {
    (value, { addInitializer }) => {
    addInitializer(function() {
    customElements.define(name, this);
    });
    }
    }

    @customElement('my-element')
    class MyElement extends HTMLElement {
    static get observedAttributes() {
    return ['some', 'attrs'];
    }
    }
    // 等同如下代码
    class MyElement {
    static get observedAttributes() {
    return ['some', 'attrs'];
    }
    }

    let initializersForMyElement = [];

    MyElement = customElement('my-element')(MyElement, {
    kind: "class",
    name: "MyElement",
    addInitializer(fn) {
    initializersForMyElement.push(fn);
    },
    }) ?? MyElement;

    for (let initializer of initializersForMyElement) {
    initializer.call(MyElement);
    }

    如果是legacy提案,我们是无法通过装饰器一步到位去注册web component的,必须手动调用customElements.define(name, xxx)

2018-09&2021-12

由于这两种用法在实际中很少特别使用这里只作简单差异化介绍

  • 2018-09
    只接收一个descriptor参数,并返回一个新的descriptor参数作为替代。
    例如方法装饰器接收的descriptor如下

    1
    2
    3
    4
    5
    6
    7
    {
    kind: "method"
    key: String, Symbol or Private Name,
    placement: "static", "prototype" or "own",
    ...Property Descriptor (argument to Object.defineProperty),
    method: The method itself
    }
  • 2021-12
    和stage3几乎完全一样,只是stage3在2021-12的基础上去除了metadata的部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    type Decorator = (value: Input, context: {
    kind: string;
    name: string | symbol;
    access: {
    get?(): unknown;
    set?(value: unknown): void;
    };
    isPrivate?: boolean;
    isStatic?: boolean;
    addInitializer?(initializer: () => void): void;
    // 被移除的getMetadata 和 setMetadataa
    getMetadata(key: symbol);
    setMetadata(key: symbol, value: unknown);
    }) => Output | void;

    metadata本文就不展开介绍,详情可参考

总结

stage3对比legacy

  1. 除了上面提到的语法区别。legacy装饰器是用“Target”(由当前被装饰目标决定是类本身还是类的原型)调用的,而在stage3中,不再提供这个Target给装饰器函数
  2. legacy装饰器会提供一个完整的descriptor对象,而stage3中只提供被装饰的值以及和它有关的上下文对象。在stage3中修改一个属性的attribute是不可能的,并且 getter 和 setter 不是“合并”而是单独被装饰

stage3对比2018-09

  1. 上面提到的语法的区别
  2. 功能上stage3是2018-09的子集

参考

tc39/proposal-decorators

  • Post title:javascript装饰器进入stage3了
  • Post author:flytam
  • Create time:2022-04-04 18:56:17
  • Post link:https://blog.flytam.vip/javascript装饰器进入stage3了.html
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.