tc39装饰器提案扩展

178 阅读7分钟

原文Extension.md):github.com/tc39/propos…

README.mdgithub.com/tc39/propos…

可能的扩展

本文件包括了在类装饰器提案框架内可能提出的后续提案。这个装饰器提案旨在允许其他语法被装饰,Stage 2 提案仅涉及类、字段、方法和访问器装饰器。主要的未解决问题是是否在初期就包括参数装饰器,因为它们在 TypeScript 中已经在类内引起了广泛的关注和使用。

注解语法

注解(Annotations) 是装饰器的声明性补充,使用语法 @{ },其行为与对象字面量完全相同。允许使用任意表达式、展开运算符、计算属性名等。可以通过被注解对象的 [Symbol.metadata] 属性访问注解内容。

@{x: "y"} @{v: "w"} class C {
  @{a: "b"} method() { }
  @{c: "d"} field;
}

C[Symbol.metadata].class.x                     // "y"
C[Symbol.metadata].class.v                     // "w"
C[Symbol.metadata].prototype.methods.method.a  // "a"
C[Symbol.metadata].instance.fields.field.c     // "d"

注解必须始终以字面量 @{ 开始。要使用现有对象作为注解,可以使用语法 @{ ...obj }。可以使用完整的对象语法,包括计算属性名、任意表达式作为值、简写名称、简洁方法等。

对于希望建立一致的注解使用约定的库和框架,可以基于它们导出的 Symbol 属性键来实现。注解在加载时间性能方面有潜在的优势,因为引擎可以直接执行它们,它们与对象字面量一样是声明性的。

注解语义对于需要关于类字段信息的情况(如 ORM 和序列化框架)可能很有用,而不会影响它们的正常运行时语义。然而,我们发现的流行生态系统示例,使用仅包含字段元数据的形式,似乎依赖于 TypeScript 类型生成的元数据。从这些示例来看,单独的注解语法似乎不足以作为解决方案。

一些框架(包括 Angular)倾向于使用添加元数据的装饰器。然而,对象字面量注解并不完全适合这种用法,因为它们无法像函数那样在 TypeScript 中进行类型检查,也不允许在保存元数据之前对其进行代码处理,并且因为它们不提供一个可用且稳定的标识符来支持像树摇这样的自定义静态分析工具。对于 Angular,使用添加元数据的装饰器可能更有意义。

因此,注解被从本提案的 "MVP"(最小可行产品)中省略,作为可能的后续提案进行考虑。

Function decorators and annotations

之前的 @logged 装饰器在函数上将直接生效(TM),并且可以与函数装饰器一起使用!

@logged
@{x: "y"}
function f() { }

f();                        // prints logging information
f[Symbol.annotations][0].x  // "y"

带有装饰器或注解的函数声明不会被提升。这是因为重新排列装饰器或注解表达式的执行顺序会显得不直观。(带有装饰器或注解的函数声明不会被提升。因为如果对装饰器或注解表达式的评估顺序进行重新排序,会变得不直观,也就是说,装饰器或注解的执行顺序会受到函数声明位置的影响,而不是像普通的函数声明那样在代码的任何地方都可以调用。)

相反,带有装饰器或注解的函数只有在达到声明时才会定义。如果它们在定义之前被使用,则会抛出一个 ReferenceError,类似于类的行为。这个 ReferenceError 条件有时被称为“暂时死区”(TDZ)。在重构时,TDZ 可能会带来不幸的情况,但至少这些情况会导致易于调试的错误,而不是运行错误的函数。

函数装饰器的详细信息:

  • 第一个参数:被装饰的函数(或者是下一个内层装饰器返回的函数)
  • 第二个参数:一个上下文对象,包含 { kind: 'function' }
  • 返回值:一个新函数,或者返回 undefined 以保持原函数不变

函数表达式内部的绑定在所有函数装饰器执行之前处于 TDZ 中。

参数装饰器和注解

参数装饰器包装函数/方法参数的值。它返回一个函数,该函数执行包装操作。

function dec(_, context) {
  assert(context.kind === "parameter");
  return arg => arg - 1;
}

function f(@dec @{x: "y"} arg) { return arg * 2 ; }

f(5)  // 8
f[Symbol.annotations].parameters[0].x  // "y"

带有装饰器或注解的参数的函数与带有装饰器/注解的函数类似:它们不会被提升,并且在其定义被执行之前处于临时死区(TDZ)。

参数装饰器的详细信息:

  • 第一个参数:undefined
  • 第二个参数:一个上下文对象,仅包含 { kind: 'parameter' }
  • 返回值:一个函数,该函数接收一个参数值并返回一个新的参数值。这个函数会以包围它的函数的 this 值为上下文调用。

这个示例可以被转换成:

let decInstance = dec(undefined, {kind: "parameter"});

function f(arg_) {
  const arg = decInstance(arg_);
  return arg * 2 ;
}

f[Symbol.annotations] = {}
f[Symbol.annotations].parameters = []
f[Symbol.annotations].parameters[0] = {x: "y"};

let 装饰器

使用 let 声明的变量可以被装饰,将它们转换为特殊的 getter/setter 配对,在变量被读取或写入时调用。

let @deprecated x = 1;
++x;  // Shows deprecation warnings for read and write

let 装饰器可能对基于局部变量的响应式系统有用,例如 "hooks" 系统。

let 装饰器的细节:

  • 第一个参数:一个 { get, set, value } 对象(其中接收者预期为 undefined,并且 value 是右侧的值)
  • 第二个参数:一个上下文对象 { kind: "let" }
  • 返回值:一个 { get, set, value } 对象,可能包含新的行为

这个例子可以被表示为:

let { get_x, set_x, value: _x } = deprecated({value: 1, get() { return _x; }, set(v) { _x = v; }}, {kind: "let"});

set_x(get_x()++);

const decorators

使用 const 声明的变量可以更简单地进行装饰——装饰器在变量定义时只是简单地包装被装饰的值。

function inc(x) { return x+1; }
const @inc x = 1;  // 2

const 装饰器的细节:

  • 第一个参数:右侧值(RHS)的值
  • 第二个参数:一个上下文对象 { kind: "const" }
  • 返回值:变量的新值

这可以被简化为如下形式:

const x = inc(1, {kind: "const"});

(这种形式本身不是特别有用,但如果未来的提案通过上下文对象共享更多信息,它可能变得更重要。)

对象字面量和属性装饰器与注解

  • 装饰过的对象字面量像类装饰器一样工作,但 kind"object"
  • 装饰过的对象字面量中的方法、getter 或 setter,工作方式与类中的相同,替换该方法。
  • 装饰过的对象属性像字段装饰器一样工作,但 kind"property",并且它接收输入中的 value 属性作为初始值,并将其返回设置输出对象中,而不是返回初始化函数,因为它只会执行一次(这样,它与 let 装饰器类似)。

例子:

const x = @decA {
  @decB p: v,
  @decC m() { }
};

块装饰器

装饰一个块可以将其包装为一个函数。

@foo { bar; }

desugar:

foo(() => bar, { kind: "block" });

类似的,在类中:

class F {
  @foo { bar; }
}

等价为:

class F {
  #_() { bar; }
  constructor() {
    foo.call(this, this.#_, {kind: "class-block"});
  }
}

这些模式可能通过类似 "hooks" 的模式和组件生命周期方法来提升使用便捷性。

请注意,这种语法只有在语句上下文中才有效;否则,它将装饰一个对象字面量。

初始化装饰器

装饰一个初始化器会将其转变为一个惰性计算函数(thunk),这样它可以在适当的上下文中执行(例如,在启用使用追踪时)或稍后重新执行(例如,当依赖发生变化时)。

let x @foo = bar;

desugar:

let x = foo(() => bar, {kind: "initializer"});

类似的,对于类:

class C {
  x @foo = bar;
}

desugar:

class C {
  #_() { return bar; }
  x = foo.call(this, this.#_);
}

这种模式可能会改善某些“computed”响应式模式。