For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-decorators.md.
  • 简体中文
  • Decorators S2.7

    中文标题:装饰器

    提案概览
    提案速览

    该提案引入了装饰器,这些函数可应用于类、类字段、方法、访问器以及一种称为自动访问器的新类元素类型,从而在不改变外部行为的情况下实现元编程。装饰器可以替换值、提供访问权限并运行初始化逻辑,上下文对象提供 kind、name、access 和 addInitializer。该提案处于第 2.

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    装饰器

    阶段:2.7

    装饰器是一个扩展 JavaScript 类的提案,在转译器环境中已被开发者广泛采用,并且对标准化有广泛的兴趣。TC39 已经对装饰器提案进行了五年多的迭代。本文档描述了一个基于过去所有提案元素的新装饰器提案。

    本 README 描述了当前的装饰器提案,该提案仍在进行中。有关此提案的先前迭代,请参阅此仓库的提交历史。

    介绍

    装饰器是在定义期间对类、类元素或其他 JavaScript 语法形式调用的函数

    @defineElement("my-class")
    class C extends HTMLElement {
      @reactive accessor clicked = false;
    }

    装饰器具有三个主要能力:

    1. 它们可以用具有相同语义的_匹配_值替换被装饰的值。(例如,装饰器可以用另一个方法替换方法,用另一个字段替换字段,用另一个类替换类,依此类推)。
    2. 它们可以通过访问器函数提供对被装饰值的访问,然后可以选择共享这些访问器函数。
    3. 它们可以初始化被装饰的值,在值被完全定义后运行额外的代码。如果该值是类的成员,则初始化每次实例发生一次。

    本质上,装饰器可以用于元编程和为值添加功能,而不会从根本上改变其外部行为。

    此提案与之前的迭代不同,在之前的迭代中,装饰器可以用完全不同类型的值替换被装饰的值。装饰器只能使用与原始值具有相同语义的值来替换值的要求实现了两个主要设计目标:

    • 使用装饰器和编写自己的装饰器都应该很容易。 以前的迭代,如_静态装饰器_提案,对于作者尤其是实现者来说都很复杂。在本提案中,装饰器是普通函数,易于访问和编写。
    • 装饰器应该影响它们装饰的东西,并避免令人困惑/非局部的影响。 以前,装饰器可以以不可预测的方式更改被装饰的值,并且还可以添加完全不相关的新值。这对_运行时_来说是有问题的,因为这意味着无法静态分析被装饰的值,对_开发者_来说也是有问题的,因为被装饰的值可能会变成完全不同类型的值,而用户没有任何提示。

    在本提案中,装饰器可以应用于以下现有类型的值:

    • 类字段(公有,私有和静态)
    • 类方法(公有,私有和静态)
    • 类访问器(公有,私有和静态)

    此外,本提案引入了一种新的可被装饰的类元素类型:

    • 类_自动访问器_,通过在类字段前应用 accessor 关键字来定义。与字段不同,它们具有 getter 和 setter,字段默认为在私有存储槽(相当于私有类字段)上获取和设置值:

      class Example {
        @reactive accessor myBool = false;
      }

    这种新的元素类型可以独立使用,并且具有独立于装饰器使用的语义。它被包含在本提案中的主要原因是,有许多装饰器的用例需要其语义,因为装饰器只能用具有相同语义的相应元素替换一个元素。这些用例在现有的装饰器生态系统中很常见,表明需要它们提供的功能。

    动机

    您可能想知道"我们到底为什么需要这些?" 装饰器是一个强大的元编程功能,可以显着简化代码,但也可能感觉"神秘",因为它们向用户隐藏了细节,使底层发生的事情更难理解。像所有抽象一样,在某些情况下,装饰器可能变得比它们的价值更麻烦。

    然而,今天仍在追求装饰器的主要原因之一,特别是 类装饰器是重要的语言特性的主要原因,是因为它们填补了 JavaScript 中元编程能力的空白。

    考虑以下函数:

    function logResult(fn) {
      return function(...args) {
        let result;
        try {
          result = fn.call(this, ...args);
          console.log(result);
        } catch (e) {
          console.error(e);
          throw e;
        }
        return result;
      }
    }
    
    const plusOne = logResult((x) => x + 1);
    
    plusOne(1); // 2

    这是 JavaScript 中每天使用的常见模式,并且是支持闭包的语言的基本能力。这是在纯 JavaScript 中实现 装饰器模式 的一个例子。您可以使用 logResult 轻松地向任何函数定义添加日志记录,并且您可以使用任意数量的"装饰器"函数来执行此操作:

    const foo = bar(baz(qux(() => /* do something cool */)))

    在其他一些语言(如 Python)中,装饰器是此模式的语法糖——它们是可以用 @ 符号应用于其他函数或直接调用它们以添加额外行为的函数。

    因此,就目前而言,在 JavaScript 中可以在函数方面使用装饰器_模式_,只是没有漂亮的 @ 语法。此模式也是_声明式_的,这一点很重要——在函数定义和装饰之间没有步骤。这意味着不可能有人意外使用未装饰的函数版本,这可能导致重大错误并使调试变得非常困难!

    然而,有一个地方我们_根本无法_使用此模式——对象和类。考虑以下类:

    class MyClass {
      x = 0;
    }

    我们如何将日志记录功能添加到 x,以便每当我们获取或设置它时,我们都记录该访问?您可以手动完成:

    class MyClass {
      #x = 0;
    
      get x() {
        console.log('getting x');
        return this.#x;
      }
    
      set x(v) {
        console.log('setting x');
        this.#x = v;
      }
    }

    但是如果我们经常这样做,那么到处添加所有这些 getter 和 setter 会很痛苦。我们可以在定义类_之后_制作一个辅助函数来为我们完成它:

    function logResult(Class, property) {
      Object.defineProperty(Class.prototype, property, {
        get() {
          console.log(`getting ${property}`);
          return this[`_${property}`];
        },
    
        set(v) {
          console.log(`setting ${property}`);
          this[`_${property}`] = v;
        }
      })
    }
    
    class MyClass {
      constructor() {
        this.x = 0;
      }
    }
    
    logResult(MyClass, 'x');

    这_有效_,但如果我们使用类字段,它会覆盖我们在原型上定义的 getter/setter,因此我们必须将赋值移动到构造函数中。它也是分多个语句完成的,因此定义本身会随着时间的推移而发生,并且不是声明式的。想象一下调试一个"定义"在多个文件中的类,每个文件在应用程序启动时添加不同的装饰。这听起来可能是一个非常糟糕的设计,但在引入类之前的过去并不少见!最后,我们无法对_私有_字段或方法执行此操作。我们不能只是替换定义。

    方法_稍微_好一些,我们可以做这样的事情:

    function logResult(fn) {
      return function(...args) {
        const result = fn.call(this, ...args);
        console.log(result);
        return result;
      }
    }
    
    class MyClass {
      x = 0;
      plusOne = logResult(() => this.x + 1);
    }

    虽然这_是_声明式的,但它也为类的每个实例创建一个新的闭包,这在规模上会带来很多额外的开销。

    通过使类装饰器成为语言特性,我们正在填补这一空白,并为类方法、字段、访问器和类本身启用装饰器模式。这允许开发者轻松地为常见任务编写抽象,例如调试日志记录、响应式编程、动态类型检查等。

    详细设计

    装饰器评估的三个步骤:

    1. 装饰器表达式(@ 后面的内容)与计算的属性名交错评估
    2. 在类定义期间,在方法被评估之后但在构造函数和原型被组装之前,调用装饰器(作为函数)。
    3. 在所有这些都被调用后,应用装饰器(修改构造函数和原型)一次性完成。

    这里的语义通常遵循 2016 年 5 月在慕尼黑举行的 TC39 会议上的共识。

    1. 评估装饰器

    装饰器作为表达式进行求值,并与计算属性名一起排序。这从左到右,从上到下进行。装饰器的结果存储在等效的局部变量中,以便在类定义最初完成执行后稍后调用。

    2. 调用装饰器

    当调用装饰器时,它们接收两个参数:

    1. 被装饰的值,对于类字段(这是一个特例),则为 undefined
    2. 一个包含有关被装饰值信息的上下文对象

    为了简洁和清晰,使用 TypeScript 接口,这是 API 的大致形式:

    type Decorator = (value: Input, context: {
      kind: string;
      name: string | symbol;
      access: {
        get?(): unknown;
        set?(value: unknown): void;
      };
      private?: boolean;
      static?: boolean;
      addInitializer(initializer: () => void): void;
    }) => Output | void;

    这里的 InputOutput 表示传递给给定装饰器以及从中返回的值。每种类型的装饰器都有不同的输入和输出,下面将更详细地介绍。所有装饰器都可以选择不返回任何内容,这默认为使用原始的、未装饰的值。

    上下文对象也根据被装饰的值而变化。分解属性:

    • kind:被装饰值的种类。这可用于断言装饰器被正确使用,或对不同类型的值有不同的行为。它是以下值之一。
      • "class"
      • "method"
      • "getter"
      • "setter"
      • "field"
      • "accessor"
    • name:值的名称,对于私有元素,则是它的_描述_(例如,可读名称)。
    • access:包含用于访问值的方法的对象。这些方法也会获取实例上元素的_最终_值,而不是传递给装饰器的当前值。这对于大多数涉及访问的用例(例如类型验证器或序列化器)都很重要。有关更多详细信息,请参阅下面的 Access 部分。
    • static:该值是否为 static 类元素。仅适用于类元素。
    • private:该值是否为私有类元素。仅适用于类元素。
    • addInitializer:允许用户为元素或类添加额外的初始化逻辑。

    有关每种类型的装饰器及其应用方式的详细分解,请参阅下面的装饰器 API 部分。

    3. 应用装饰器

    装饰器在所有装饰器被调用后应用。装饰器应用算法的中间步骤是不可观察的——新构造的类在所有方法和非静态字段装饰器被应用之前是不可用的。

    类装饰器仅在所有方法和字段装饰器被调用并应用后被调用。

    最后,静态字段被执行和应用。

    语法

    此装饰器提案使用先前阶段 2 装饰器提案的语法。这意味着:

    • 装饰器表达式被限制为变量、使用 . 但不使用 [] 的属性访问以及调用 () 组成的链。要使用任意表达式作为装饰器,@(expression) 是一个逃生舱口。
    • 类表达式可以被装饰,而不仅仅是类声明。
    • 类装饰器只能出现在 export/export default 之前或之后。

    没有用于定义装饰器的特殊语法;任何函数都可以作为装饰器应用。

    装饰器 API

    类方法
    type ClassMethodDecorator = (value: Function, context: {
      kind: "method";
      name: string | symbol;
      access: { get(): unknown };
      static: boolean;
      private: boolean;
      addInitializer(initializer: () => void): void;
    }) => Function | void;

    类方法装饰器接收被装饰的方法作为第一个值,并且可以选择返回一个新方法来替换它。如果返回新方法,它将替换原型上的原始方法(对于静态方法,则是类本身上的原始方法)。如果返回任何其他类型的值,则会抛出错误。

    方法装饰器的一个例子是 @logged 装饰器。此装饰器接收原始函数,并返回一个新函数,该函数包装原始函数并在调用前后记录日志。

    function logged(value, { kind, name }) {
      if (kind === "method") {
        return function (...args) {
          console.log(`starting ${name} with arguments ${args.join(", ")}`);
          const ret = value.call(this, ...args);
          console.log(`ending ${name}`);
          return ret;
        };
      }
    }
    
    class C {
      @logged
      m(arg) {}
    }
    
    new C().m(1);
    // starting m with arguments 1
    // ending m

    此示例大致"脱糖"为以下内容(即,可以这样转译):

    class C {
      m(arg) {}
    }
    
    C.prototype.m = logged(C.prototype.m, {
      kind: "method",
      name: "m",
      static: false,
      private: false,
    }) ?? C.prototype.m;
    类访问器
    type ClassGetterDecorator = (value: Function, context: {
      kind: "getter";
      name: string | symbol;
      access: { get(): unknown };
      static: boolean;
      private: boolean;
      addInitializer(initializer: () => void): void;
    }) => Function | void;
    
    type ClassSetterDecorator = (value: Function, context: {
      kind: "setter";
      name: string | symbol;
      access: { set(value: unknown): void };
      static: boolean;
      private: boolean;
      addInitializer(initializer: () => void): void;
    }) => Function | void;

    访问器装饰器接收原始的底层 getter/setter 函数作为第一个值,并且可以选择返回一个新的 getter/setter 函数来替换它。与方法装饰器一样,这个新函数被放置在原型上以替换原始函数(对于静态访问器,则是类上),如果返回任何其他类型的值,则会抛出错误。

    访问器装饰器_分别_应用于 getter 和 setter。在以下示例中,@foo 仅应用于 get x() - set x() 未被装饰:

    class C {
      @foo
      get x() {
        // ...
      }
    
      set x(val) {
        // ...
      }
    }

    我们可以扩展我们之前为方法定义的 @logged 装饰器来处理访问器。代码基本相同,我们只需要处理额外的 kinds。

    function logged(value, { kind, name }) {
      if (kind === "method" || kind === "getter" || kind === "setter") {
        return function (...args) {
          console.log(`starting ${name} with arguments ${args.join(", ")}`);
          const ret = value.call(this, ...args);
          console.log(`ending ${name}`);
          return ret;
        };
      }
    }
    
    class C {
      @logged
      set x(arg) {}
    }
    
    new C().x = 1
    // starting x with arguments 1
    // ending x

    此示例大致"脱糖"为以下内容(即,可以这样转译):

    class C {
      set x(arg) {}
    }
    
    let { set } = Object.getOwnPropertyDescriptor(C.prototype, "x");
    set = logged(set, {
      kind: "setter",
      name: "x",
      static: false,
      private: false,
    }) ?? set;
    
    Object.defineProperty(C.prototype, "x", { set });
    类字段
    type ClassFieldDecorator = (value: undefined, context: {
      kind: "field";
      name: string | symbol;
      access: { get(): unknown, set(value: unknown): void };
      static: boolean;
      private: boolean;
      addInitializer(initializer: () => void): void;
    }) => (initialValue: unknown) => unknown | void;

    与方法不同,访问器,类字段在被装饰时没有直接的输入值。相反,用户可以选择返回一个初始化函数,该函数在字段被赋值时运行,接收字段的初始值并返回一个新的初始值。如果返回的函数以外的任何其他类型的值,则会抛出错误。

    我们可以扩展我们的 @logged 装饰器来处理类字段,在字段被赋值时记录日志以及值是什么。

    function logged(value, { kind, name }) {
      if (kind === "field") {
        return function (initialValue) {
          console.log(`initializing ${name} with value ${initialValue}`);
          return initialValue;
        };
      }
    
      // ...
    }
    
    class C {
      @logged x = 1;
    }
    
    new C();
    // initializing x with value 1

    此示例大致"脱糖"为以下内容(即,可以这样转译):

    let initializeX = logged(undefined, {
      kind: "field",
      name: "x",
      static: false,
      private: false,
    }) ?? (initialValue) => initialValue;
    
    class C {
      x = initializeX.call(this, 1);
    }

    初始化函数以类的实例作为 this 调用,因此字段装饰器也可用于引导注册关系。例如,您可以在父类上注册子元素:

    const CHILDREN = new WeakMap();
    
    function registerChild(parent, child) {
      let children = CHILDREN.get(parent);
    
      if (children === undefined) {
        children = [];
        CHILDREN.set(parent, children);
      }
    
      children.push(child);
    }
    
    function getChildren(parent) {
      return CHILDREN.get(parent);
    }
    
    function register() {
      return function(value) {
        registerChild(this, value);
    
        return value;
      }
    }
    
    class Child {}
    class OtherChild {}
    
    class Parent {
      @register child1 = new Child();
      @register child2 = new OtherChild();
    }
    
    let parent = new Parent();
    getChildren(parent); // [Child, OtherChild]
    type ClassDecorator = (value: Function, context: {
      kind: "class";
      name: string | undefined;
      addInitializer(initializer: () => void): void;
    }) => Function | void;

    类装饰器接收被装饰的类作为第一个参数,并且可以选择返回一个新的可调用对象(类、函数或 Proxy)来替换它。如果返回非可调用值,则会抛出错误。

    我们可以进一步扩展我们的 @logged 装饰器,以便在创建类的实例时记录日志:

    function logged(value, { kind, name }) {
      if (kind === "class") {
        return class extends value {
          constructor(...args) {
            super(...args);
            console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`);
          }
        }
      }
    
      // ...
    }
    
    @logged
    class C {}
    
    new C(1);
    // constructing an instance of C with arguments 1

    此示例大致"脱糖"为以下内容(即,可以这样转译):

    class C {}
    
    C = logged(C, {
      kind: "class",
      name: "C",
    }) ?? C;
    
    new C(1);

    如果被装饰的类是匿名类,则 context 对象的 name 属性为 undefined

    新的类元素

    类自动访问器

    类自动访问器是一种新的构造,通过在类字段前添加 accessor 关键字来定义:

    class C {
      accessor x = 1;
    }

    与常规字段不同,自动访问器在类原型上定义 getter 和 setter。此 getter 和 setter 默认为获取和设置私有槽上的值。以上大致脱糖为:

    class C {
      #x = 1;
    
      get x() {
        return this.#x;
      }
    
      set x(val) {
        this.#x = val;
      }
    }

    也可以定义静态和私有自动访问器:

    class C {
      static accessor x = 1;
      accessor #y = 2;
    }

    自动访问器可以被装饰,自动访问器装饰器具有以下签名:

    type ClassAutoAccessorDecorator = (
      value: {
        get: () => unknown;
        set(value: unknown) => void;
      },
      context: {
        kind: "accessor";
        name: string | symbol;
        access: { get(): unknown, set(value: unknown): void };
        static: boolean;
        private: boolean;
        addInitializer(initializer: () => void): void;
      }
    ) => {
      get?: () => unknown;
      set?: (value: unknown) => void;
      init?: (initialValue: unknown) => unknown;
    } | void;

    与字段装饰器不同,自动访问器装饰器接收一个值,该值是包含在类的原型(对于静态自动访问器,则是类本身)上定义的 getset 访问器的对象。然后,装饰器可以包装这些并返回_新的_ get 和/或 set,从而允许装饰器拦截对属性的访问。这是字段无法实现的能力,但自动访问器可以实现。此外,自动访问器可以返回一个 init 函数,该函数可用于更改私有槽中后备值的初始值,类似于字段装饰器。如果返回一个对象但省略了任何值,则省略值的默认行为是使用原始行为。如果返回除包含这些属性的对象以外的任何其他类型的值,则会抛出错误。

    进一步扩展 @logged 装饰器,我们可以让它也处理自动访问器,在自动访问器被初始化和访问时记录日志:

    function logged(value, { kind, name }) {
      if (kind === "accessor") {
        let { get, set } = value;
    
        return {
          get() {
            console.log(`getting ${name}`);
    
            return get.call(this);
          },
    
          set(val) {
            console.log(`setting ${name} to ${val}`);
    
            return set.call(this, val);
          },
    
          init(initialValue) {
            console.log(`initializing ${name} with value ${initialValue}`);
            return initialValue;
          }
        };
      }
    
      // ...
    }
    
    class C {
      @logged accessor x = 1;
    }
    
    let c = new C();
    // initializing x with value 1
    c.x;
    // getting x
    c.x = 123;
    // setting x to 123

    此示例大致"脱糖"为以下内容:

    class C {
      #x = initializeX.call(this, 1);
    
      get x() {
        return this.#x;
      }
    
      set x(val) {
        this.#x = val;
      }
    }
    
    let { get: oldGet, set: oldSet } = Object.getOwnPropertyDescriptor(C.prototype, "x");
    
    let {
      get: newGet = oldGet,
      set: newSet = oldSet,
      init: initializeX = (initialValue) => initialValue
    } = logged(
      { get: oldGet, set: oldSet },
      {
        kind: "accessor",
        name: "x",
        static: false,
        private: false,
      }
    ) ?? {};
    
    Object.defineProperty(C.prototype, "x", { get: newGet, set: newSet });

    使用 addInitializer 添加初始化逻辑

    addInitializer 方法在提供给装饰器的上下文对象上可用于每种类型的值。可以调用此方法将初始化函数与类或类元素关联起来,该函数可用于在值定义后运行任意代码以完成其设置。这些初始化的时机取决于装饰器的类型:

    • 类装饰器在_类被完全定义_之后,并且_在_类静态字段被赋值_之后_。
    • 类静态元素
      • 方法以及 Getter/Setter 装饰器:在类定义期间,在_静态类方法被赋值_之后在任何_静态类字段被初始化_之前
      • 字段和访问器装饰器:在类定义期间,在它们应用的字段或访问器被初始化_之后_立即
    • 类非静态元素
      • 方法以及 Getter/Setter 装饰器:在类构造期间,在任何_类字段被初始化_之前
      • 字段和访问器装饰器:在类构造期间,在它们应用的字段或访问器被初始化_之后_立即
    示例:@customElement

    我们可以将 addInitializer 与类装饰器一起使用,以创建在浏览器中注册 Web 组件的装饰器。

    function customElement(name) {
      return (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);
    }
    示例:@bound

    我们还可以将 addInitializer 与方法装饰器一起使用来创建 @bound 装饰器,它将方法绑定到类的实例:

    function bound(value, { name, addInitializer }) {
      addInitializer(function () {
        this[name] = this[name].bind(this);
      });
    }
    
    class C {
      message = "hello!";
    
      @bound
      m() {
        console.log(this.message);
      }
    }
    
    let { m } = new C();
    
    m(); // hello!

    此示例大致"脱糖"为以下内容:

    class C {
      constructor() {
        for (let initializer of initializersForM) {
          initializer.call(this);
        }
    
        this.message = "hello!";
      }
    
      m() {}
    }
    
    let initializersForM = []
    
    C.prototype.m = bound(
      C.prototype.m,
      {
        kind: "method",
        name: "m",
        static: false,
        private: false,
        addInitializer(fn) {
          initializersForM.push(fn);
        },
      }
    ) ?? C.prototype.m;

    访问和元数据旁路

    到目前为止,我们已经了解了如何使用装饰器替换值,但我们还没有看到如何使用装饰器 access 对象。这是依赖注入装饰器的示例,它通过元数据旁路使用此对象来在实例上注入值。

    const INJECTIONS = new WeakMap();
    
    function createInjections() {
      const injections = [];
    
      function injectable(Class) {
        INJECTIONS.set(Class, injections);
      }
    
      function inject(injectionKey) {
        return function applyInjection(v, context) {
          injections.push({ injectionKey, set: context.access.set });
        };
      }
    
      return { injectable, inject };
    }
    
    class Container {
      registry = new Map();
    
      register(injectionKey, value) {
        this.registry.set(injectionKey, value);
      }
    
      lookup(injectionKey) {
        this.registry.get(injectionKey);
      }
    
      create(Class) {
        let instance = new Class();
    
        for (const { injectionKey, set } of INJECTIONS.get(Class) || []) {
          set.call(instance, this.lookup(injectionKey));
        }
    
        return instance;
      }
    }
    
    class Store {}
    
    const { injectable, inject } = createInjections();
    
    @injectable
    class C {
      @inject('store') store;
    }
    
    let container = new Container();
    let store = new Store();
    
    container.register('store', store);
    
    let c = container.create(C);
    
    c.store === store; // true

    访问通常基于该值是用于读取还是写入的。字段和自动访问器可以读取和写入。访问器可以是 getter 的读取或 setter 的写入。方法只能被读取。

    可能的扩展

    进一步构造上的装饰器在 EXTENSIONS.md 中进行研究。

    标准化计划

    • 在提案中迭代开放问题,将它们提交给 TC39,并在双周装饰器电话会议中进一步讨论,以便在未来的会议上向委员会得出结论
      • 状态:开放问题已解决,装饰器工作组已就设计达成总体共识。
    • 编写规范文本
      • 状态:完成,可在此处获得 这里
    • 在实验性转译器中实现
    • 收集测试转译器实现的 JavaScript 开发者的反馈
    • 提议进入阶段 3。

    常见问题解答

    我今天应该如何在转译器中使用装饰器?

    由于装饰器已达到阶段 3 并接近完成,现在建议新项目使用阶段 3 装饰器的最新转换。这些在 Babel、TypeScript 和其他流行的构建工具中可用。

    现有项目应开始为其生态系统制定升级计划。在大多数情况下,应该可以通过匹配传递给装饰器的参数来同时支持旧版和阶段 3 版本。在少数情况下,由于两个版本之间的能力差异,这可能无法实现。如果您遇到这种情况,请在此仓库上提出问题进行讨论!

    此提案与其他版本的装饰器相比如何?

    与 Babel "旧版" 装饰器的比较

    Babel 旧模式装饰器基于 2014 年 JavaScript 装饰器提案的状态。除了上面列出的语法更改外,Babel 旧版装饰器的调用约定与此提案不同:

    • 旧版装饰器使用"目标"(正在构造的类或原型)调用,而正在构造的类在此提案中不可用于装饰器。
    • 旧版装饰器使用完整的属性描述符调用,而此提案使用"被装饰的东西"和上下文对象调用装饰器。这意味着,例如,无法更改属性属性,并且 getter 和 setter 不会"合并",而是分别装饰。

    尽管存在这些差异,但通常应该可以使用此装饰器提案实现与 Babel 旧版装饰器相同的功能。如果您在此提案中看到重要的缺失功能,请提出问题。

    与 TypeScript "实验性" 装饰器的比较

    TypeScript 实验性装饰器与 Babel 旧版装饰器大体相似,因此该部分的评论也适用。此外:

    • 此提案不包括参数装饰器,但未来的内置装饰器可能会提供它们,请参阅 EXTENSIONS.md
    • TypeScript 装饰器在所有静态装饰器之前运行所有实例装饰器,而此提案中的评估顺序基于程序中的顺序,无论它们是静态的还是实例。

    尽管存在这些差异,但通常应该可以使用此装饰器提案实现与 TypeScript 实验性装饰器相同的功能。如果您在此提案中看到重要的缺失功能,请提出问题。

    与之前的阶段 2 装饰器提案的比较

    之前的阶段 2 装饰器提案比此提案功能更全面,包括:

    • 所有装饰器添加任意'额外'类元素的能力,而不仅仅是包装/更改被装饰的元素。
    • 声明新的私有字段的能力,包括在多个类中重用私有名称
    • 类装饰器访问以操作类中的所有字段和方法
    • 更灵活地处理初始化器,将其视为"thunk"

    之前的阶段 2 装饰器提案基于描述符的概念,这些描述符代表各种类元素。此提案中不存在此类描述符。然而,这些描述符给类形状带来了过多的灵活性/动态性,以至于无法有效优化。

    此装饰器提案故意省略了这些功能,以保持装饰器的含义"范围明确"和直观,并简化转译器和原生引擎中的实现。

    与"静态装饰器"提案的比较

    静态装饰器是一种包含一组内置装饰器,并支持源自它们的用户定义装饰器的想法。静态装饰器位于单独的命名空间中,以支持静态可分析性。

    静态装饰器提案既存在过度复杂的问题,也存在优化不足的问题。此提案通过回归到装饰器是普通函数的常见模型来避免这种复杂性。

    有关静态装饰器提案缺乏可优化性的更多信息,请参阅 V8 对装饰器可优化性的分析,此提案旨在解决该问题。

    如果以前的 TC39 装饰器提案没有成功,为什么不回去标准化 TS/Babel 旧版装饰器?

    可优化性:此装饰器提案和旧版装饰器在装饰器是函数这一点上是共同的。然而,此提案的调用约定旨在通过以下更改与旧版装饰器相比,使引擎更可优化:

    • 正在构造的不完整类不会暴露给装饰器,因此它不需要在类定义评估期间可观察地经历形状变化。
    • 只能更改被装饰的构造的内容;属性描述符的"形状"不能更改。

    与 [[Define]] 字段语义不兼容:旧版装饰器在应用于字段声明时,深度依赖于字段初始化器调用 setter 的语义。TC39 得出结论,字段声明应该像 Object.defineProperty 那样工作。这个决定使得许多使用旧版装饰器的模式不再有效。虽然 Babel 提供了一种通过将初始化器作为 thunk 可用来解决此问题的方法,但实现者拒绝这些语义,因为它们增加了运行时成本。

    为什么优先考虑"旧版"装饰器的功能(如类),而不是装饰器可以提供的其他功能?

    "旧版"装饰器在 JavaScript 生态系统中已经变得非常流行。这证明它们确实有所发现,并解决了许多人面临的问题。此提案采纳了这些知识并加以运用,在 JavaScript 语言中构建了原生支持。它这样做的方式为将来使用相同的语法进行更多不同类型的扩展留下了机会,如 EXTENSIONS.md 中所述。

    我们能否支持装饰对象、参数、块、函数等?

    是的!一旦我们验证了这个核心方法,这个提案的作者计划回来为更多种类的装饰器提出建议。特别是,鉴于 TypeScript 参数装饰器的流行,我们正在考虑将此提案的初始版本中包含参数装饰器。请参阅 EXTENSIONS.md

    装饰器会让您访问私有字段和方法吗?

    是的,私有字段和方法可以像普通字段和方法一样被装饰。唯一的区别是上下文对象上的 name 键只是元素的描述,而不是我们可以用来访问它的东西。相反,提供了包含 get/set 函数的 access 对象。请参阅标题为"访问"下的示例。

    在实现后,应如何在转译器中使用这个新提案?

    此装饰器提案需要与以前的旧版/实验性装饰器语义分开的转译器实现。可以通过构建时选项(例如,命令行标志或配置文件中的条目)切换到新语义。请注意,此提案预计在阶段 3 之前会继续发生重大变化,不应指望其稳定性。

    导出装饰器的模块可以通过检查它们的第二个参数是否是对象(在此提案中,始终是;以前,始终不是)来轻松检查它们是以旧版/实验性方式调用还是以本文所述的方式调用。因此,应该可以维护适用于这两种方法的装饰器库。

    是什么让这个装饰器提案比以前的提案更具静态可分析性?这个提案是否仍然是静态可分析的,即使它基于运行时值?

    在这个装饰器提案中,每个装饰器位置都对脱糖后生成的代码形状产生一致的影响。系统不会使用动态值调用 Object.defineProperty 来设置属性属性,并且从用户定义的装饰器中进行此类调用也是不切实际的,因为未向装饰器提供"目标";只提供函数的实际内容。

    静态可分析性如何帮助转译器和其他工具?

    静态可分析的装饰器帮助工具从构建工具生成更快、更小的 JavaScript,使装饰器可以被转译掉,而不会在运行时创建和操作额外的数据结构。工具更容易理解正在发生的事情,这可能有助于摇树、类型系统等。

    LinkedIn 尝试使用之前的阶段 2 装饰器提案发现它导致了显着的性能开销。Polymer 和 TypeScript 团队的成员也注意到使用这些装饰器生成的代码大小显着增加。

    相比之下,此装饰器提案应该被编译为在特定位置进行函数调用,并将一个类元素替换为另一个类元素。我们正在通过在 Babel 中实现该提案来证明这一好处,以便在提议进入阶段 3 之前可以进行比较。

    静态可分析性有助于工具的另一个案例是 ES 模块的命名导出。命名导入和导出的固定性质有助于摇树、类型的导入和导出,并且在这里,作为组合装饰器可预测性质的基础。尽管生态系统仍在从导出完全动态的对象过渡,但 ES 模块已在工具中扎根并被认为是有用的,正是因为它们更静态的性质。

    静态可分析性如何帮助原生 JS 引擎?

    尽管 JIT 可以优化掉几乎所有东西,但它只能在程序"预热"之后这样做。也就是说,当典型的 JavaScript 引擎启动时,它不使用 JIT——相反,它将 JavaScript 编译为字节码并直接执行。稍后,如果代码运行很多次,JIT 将启动并优化程序。

    对流行 Web 应用程序执行跟踪的研究表明,启动页面的大部分时间通常花在通过字节码进行解析和执行上,通常只有较小百分比的时间运行 JIT 优化的代码。这意味着,如果我们希望 Web 快速,我们不能依赖花哨的 JIT 优化。

    装饰器,尤其是之前的阶段 2 提案,增加了各种开销来源,无论是执行类定义还是使用类,如果它们没有被 JIT 优化掉,都会使启动变慢。相比之下,组合装饰器总是以固定的方式归结为内置装饰器,可以直接由字节码生成处理。

    getter/setter 对的合并发生了什么?

    此装饰器提案基于一个通用模型,其中每个装饰器只影响一个语法元素——字段、方法、getter、setter 或类。可以立即看到被装饰的内容。

    先前的"阶段 2"装饰器提案有一个"合并"getter/setter 对的步骤,这最终有点类似于旧版装饰器对属性描述符的操作方式。然而,由于访问器的计算属性名的动态性,这种合并在规范和实现中都变得非常复杂。在"阶段 2"装饰器的 polyfill 实现中,合并是开销(例如,在代码大小方面)的一大来源。

    目前尚不清楚哪些用例受益于 getter/setter 合并。删除 getter/setter 合并且大大简化了规范,我们希望它也能简化实现。

    如果您对此有进一步的想法,请参与问题跟踪器上的讨论:#256

    为什么装饰器花了这么长时间?

    我们对这里的延迟深表歉意。我们理解这会在 JavaScript 生态系统中造成真正的问题,并且正在尽快努力解决问题。

    我们花了很长时间才让每个人就跨框架、工具和原生实现的需求达成一致。只有在对各种具体方向进行推动之后,我们才完全理解此提案旨在满足的需求。

    我们正在努力在 TC39 内部以及与更广泛的 JavaScript 社区之间建立更好的沟通,以便将来可以更早地纠正此类问题。