跳到主要内容

语法特性-Decorators

Decorator的历史

  • 2014年4月10日, Decorators在TC39上呗提出. 这个提案被添加到TC39的stage0阶段
  • 2014年10月22日, Angular团队宣布Angular2.0会使用AtScript并且编译到js和Dart. 他们计划在AtScript中支持:
    • 三种类型注释:
      • 类型注释
      • 字段树注释显式声明
      • 元数据注释, 具有和装饰器相同的语法, 但是只添加元数据, 不改带注释的构造的工作方式
    • 运行时的类型检查
    • type introspection: 类型内省
  • 2015年1月28日, angular和ts团队交换了一些想法, 3月5日, angular和ts团队宣布angular从AtScript直接切换到使用TpeScript, Typescript会支持一些AtScript的特性(尤其是装饰器的部分)
  • 2015年3月24日, decorator 提案被推进到stage1,
  • 2015年7月20日, ts1.5发布, 并且支持使用experimentalDecorators开启stage1下的装饰器越发
  • 2016年7月28日, decorator 被推进到stage2.
  • 2022年3月28日, decorator 被推进到stage3.

什么是 Decorators

Decorators可以让我们改变使用js的构造函数(class或者其成员方法), 比如:

class C {
@trace
toString() {
return 'C';
}
}

实现trace只需要写一个函数:

function trace(decoratedMethod) {
// Returns a function that replaces `decoratedMethod`.
}

上面的class C等价于:

class C {
toString() {
return 'C';
}
}
C.prototype.toString = trace(C.prototype.toString);

换句话说, 一个Decorators就是一个能够用来当一个构造器的函数.

编写和使用Decorators是元编程的一种:

  • 我们不写直接处理数据的代码(编程)
  • 我们写的代码会用于处理用户代码(元编程)

Decorator Function

装饰器函数在TS的中类型声明大概是这样的:

type Decorator = (
value: DecoratedValue, // only fields differ
context: {
kind: string;
name: string | symbol;
addInitializer(initializer: () => void): void;

// Don’t always exist:
static: boolean;
private: boolean;
access: {get: () => unknown, set: (value: unknown) => void};
}
) => void | ReplacementValue; // only fields differ

一个装饰器函数, 一般来说是一个函数

  • value: 装饰器装饰的函数(类或成员)
  • context对象:
    • 附加的value的信息
    • 一个简单的api, 用于进行元编程

kind属性告诉装饰器是用在哪个JS构造器上, 这样我们可以用同样的装饰器来应用到多个目标的构造器.

当前来说, 双时期可以用在:

  • class
  • method
  • getter
  • setter
  • accessor(一个新的类成员, 稍后介绍)
  • filed

其类型:

type Decorator =
| ClassDecorator
| ClassMethodDecorator
| ClassGetterDecorator
| ClassSetterDecorator
| ClassAutoAccessorDecorator
| ClassFieldDecorator
;

装饰器的作用

每个装饰器都可以有四个方面的作用:

  • 它可以通过更改参数值来更改修饰的实体。
  • 它可以通过返回兼容的值来替换修饰的实体.
    • Compatible, 装饰器必须返回相同类型的值, 比如class的装饰器必须返回一个可构造的值
    • 如果装饰器不想替换装饰值,它可以通过不返回任何内容来显式或隐式返回 undefined
  • 向其他人展示对装饰实体的访问权限。上下文access使它能够通过其方法.get().set()实现这一点。
  • 处理修饰实体及其容器(如果它有一个),在两者都存在之后:该功能由context.addInitializer提供。它允许装饰器注册一个初始值设定项——一个在一切就绪时调用的回调(稍后将解释更多细节)。

能力1: 替换装饰实体

举例:

function replaceMethod() {
return function () { // (A)
return `How are you, ${this.name}?`;
}
}

class Person {
constructor(name) {
this.name = name;
}
@replaceMethod
hello() { // (B)
return `Hi ${this.name}!`;
}
}

const robin = new Person('Robin');
assert.equal(
robin.hello(), 'How are you, Robin?'
);

在这个例子中, 装饰器@replaceMethodhello方法替成了自己返回的函数.

能力2: 向其他人公开对装饰实体的访问

let acc;
function exposeAccess(_value, {access}) {
acc = access;
}

class Color {
@exposeAccess
name = 'green'
}

const green = new Color();
assert.equal(
green.name, 'green'
);
// Using `acc` to get and set `green.name`
assert.equal(
acc.get.call(green), 'green'
);
acc.set.call(green, 'red');
assert.equal(
green.name, 'red'
);

装饰将一个对象存储在变量ace中, 该变量允许我们访问green的实例属性color

能力3: 处理装饰实体及其容器

function collect(_value, {name, addInitializer}) {
addInitializer(function () { // (A)
if (!this.collectedMethodKeys) {
this.collectedMethodKeys = new Set();
}
this.collectedMethodKeys.add(name);
});
}

class C {
@collect
toString() {}
@collect
[Symbol.iterator]() {}
}
const inst = new C();
assert.deepEqual(
inst.collectedMethodKeys,
new Set(['toString', Symbol.iterator])
);

初始化装饰器必须是普通函数(非箭头), 因为this需要访问隐式采纳数. 箭头函数不提供这种访问, 他们的this是静态作用域的.

汇总

装饰器类型(input) => output.access
Class(func) => func2
Method(func) => func2{get}
Getter(func) => func2{get}
Setter(func) => func2{set}
Auto-accessor({get,set}) => {get,set,init}{get,set}
Field() => (initValue)=>initValue2{get,set}

其中每个函数中this的值如下:

this is →undefinedClassInstance
Decorator function
Static initializer
Non-static initializer
Static field decorator result
Non-static field decorator result

Decorators 的语法和语义

表达式语法

@(<<expr>>)

我们可以连续使用多个装饰器:

@myFunc
@myFuncFactory('arg1', 'arg2')

@libraryModule.prop
@someObj.method(123)

@(wrap(dict['prop'])) // arbitrary expression

class MyClass {}

装饰器的执行顺序

  • @评估: 在类定义的执行期间符号后的表达式, 以及计算的属性键和静态字段. 结果必须是一个函数, 他们存储在临时位置以便稍后调用
  • 调用: 装饰器函数在类定义的执行期间被调用, 在方法被评估之后, 构造函数和原型被组装之前. 其执行结果再次被存储在临时的位置
  • 应用: 调用所有装饰器函数后, 使用他们的结果, 影响构造函数和原型.
function decorate(str) {
console.log(`EVALUATE @decorate(): ${str}`);
return () => console.log(`APPLY @decorate(): ${str}`); // (A)
}
function log(str) {
console.log(str);
return str;
}

@decorate('class')
class TheClass {

@decorate('static field')
static staticField = log('static field value');

@decorate('prototype method')
[log('computed key')]() {}

@decorate('instance field')
instanceField = log('instance field value');
// This initializer only runs if we instantiate the class
}

// Output:
// EVALUATE @decorate(): class
// EVALUATE @decorate(): static field
// EVALUATE @decorate(): prototype method
// computed key
// EVALUATE @decorate(): instance field
// APPLY @decorate(): prototype method
// APPLY @decorate(): static field
// APPLY @decorate(): instance field
// APPLY @decorate(): class
// static field value

装饰器初始化器运行的时机

  • 类装饰器初始化器在类被完全定义并且所有静态字段都被初始化之后运行
  • 非静态类元素的初始化器在实例化期间运行, 在实例字段被初始化之前
  • 静态类元素装饰器的初始化器在类定义期间运行,在定义静态字段之前但在定义其他所有类元素之后

从装饰器中暴露数据

将暴露数据存储在周边范围内

比如:

const classes = new Set(); // (A)

function collect(value, {kind, addInitializer}) {
if (kind === 'class') {
classes.add(value);
}
}

@collect
class A {}
@collect
class B {}
@collect
class C {}

assert.deepEqual(
classes, new Set([A, B, C])
);

通过工厂函数管理暴露的数据

function createClassCollector() {
const classes = new Set();
function collect(value, {kind, addInitializer}) {
if (kind === 'class') {
classes.add(value);
}
}
return {
classes,
collect,
};
}

const {classes, collect} = createClassCollector();

@collect
class A {}
@collect
class B {}
@collect
class C {}

assert.deepEqual(
classes, new Set([A, B, C])
);

通过类管理暴露的数据

class ClassCollector {
classes = new Set();
install = (value, {kind}) => { // (A)
if (kind === 'class') {
this.classes.add(value); // (B)
}
};
}

const collector = new ClassCollector();

@collector.install
class A {}
@collector.install
class B {}
@collector.install
class C {}

类装饰器

type ClassDecorator = (
value: Function,
context: {
kind: 'class';
name: string | undefined;
addInitializer(initializer: () => void): void;
}
) => Function | void;

类装饰器的能力:

  • 改变装饰类的值
  • 通过返回可调用的值来替换装饰类
  • 注册初始化器, 在装饰类完全设置后调用
  • 得不到context.access, 因为累不是其他语言结构的成员(例如: 方法是类的成员)

例子: 收集实例

class InstanceCollector {
instances = new Set();
install = (value, {kind}) => {
if (kind === 'class') {
const _this = this;
return function (...args) { // (A)
const inst = new value(...args); // (B)
_this.instances.add(inst);
return inst;
};
}
};
}

const collector = new InstanceCollector();

@collector.install
class MyClass {}

const inst1 = new MyClass();
const inst2 = new MyClass();
const inst3 = new MyClass();

assert.deepEqual(
collector.instances, new Set([inst1, inst2, inst3])
);

我们可以通过装饰器收集给定类的所有实例.

注意: 我们不能在A行返回箭头函数, 因为箭头函数不能被新调用.

这个例子的缺点在于它破坏了instanceof

assert.equal(
inst1 instanceof MyClass,
false
);

我们有一些方法来解决这个问题:

1. 用.prototype手动设置instanceof:

function countInstances(value) {
const _this = this;
let instanceCount = 0;
// The wrapper must be new-callable
const wrapper = function (...args) {
instanceCount++;
const instance = new value(...args);
// Change the instance
instance.count = instanceCount;
return instance;
};
wrapper.prototype = value.prototype; // (A)
return wrapper;
}

@countInstances
class MyClass {}

const inst1 = new MyClass();
assert.ok(inst1 instanceof MyClass);
assert.equal(inst1.count, 1);

const inst2 = new MyClass();
assert.ok(inst2 instanceof MyClass);
assert.equal(inst2.count, 2);

因为:

inst instanceof C
C.prototype.isPrototypeOf(inst) // 等价于

这两者是等价的

2. 用Symbol.hasInstance

function countInstances(value) {
const _this = this;
let instanceCount = 0;
// The wrapper must be new-callable
const wrapper = function (...args) {
instanceCount++;
const instance = new value(...args);
// Change the instance
instance.count = instanceCount;
return instance;
};
// Property is ready-only, so we can’t use assignment
Object.defineProperty( // (A)
wrapper, Symbol.hasInstance,
{
value: function (x) {
return x instanceof value;
}
}
);
return wrapper;
}

@countInstances
class MyClass {}

const inst1 = new MyClass();
assert.ok(inst1 instanceof MyClass);
assert.equal(inst1.count, 1);

const inst2 = new MyClass();
assert.ok(inst2 instanceof MyClass);
assert.equal(inst2.count, 2);

3. 通过子类启用instanceof

function countInstances(value) {
const _this = this;
let instanceCount = 0;
// The wrapper must be new-callable
return class extends value { // (A)
constructor(...args) {
super(...args);
instanceCount++;
// Change the instance
this.count = instanceCount;
}
};
}

@countInstances
class MyClass {}

const inst1 = new MyClass();
assert.ok(inst1 instanceof MyClass);
assert.equal(inst1.count, 1);

const inst2 = new MyClass();
assert.ok(inst2 instanceof MyClass);
assert.equal(inst2.count, 2);

类方法装饰器

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

方法装饰器:

  • 可以通过改变装饰的方法
  • 可以通过返回一个函数来替换装饰方法
  • 可以注册初始值设定项
  • context.access只能获取属性的值, 不支持设置

构造函数不能被装饰, 他们只是看起来像方法, 但实际上并不是方法

例子: 跟踪方法调用

装饰器@trace可以用来跟踪方法的调用和结果记录:

function trace(value, {kind, name}) {
if (kind === 'method') {
return function (...args) {
console.log(`CALL ${name}: ${JSON.stringify(args)}`);
const result = value.apply(this, args);
console.log('=> ' + JSON.stringify(result));
return result;
};
}
}

class StringBuilder {
#str = '';
@trace
add(str) {
this.#str += str;
}
@trace
toString() {
return this.#str;
}
}

const sb = new StringBuilder();
sb.add('Home');
sb.add('page');
assert.equal(
sb.toString(), 'Homepage'
);

// Output:
// CALL add: ["Home"]
// => undefined
// CALL add: ["page"]
// => undefined
// CALL toString: []
// => "Homepage"

例子: 将方法绑定到实例

通常我们提取方法后, this的绑定会丢失, 我们可以通过装饰器来强绑定:

function bind(value, {kind, name, addInitializer}) {
if (kind === 'method') {
addInitializer(function () { // (B)
this[name] = value.bind(this); // (C)
});
}
}

class Color2 {
#name;
constructor(name) {
this.#name = name;
}
@bind
toString() {
return `Color(${this.#name})`;
}
}

const green2 = new Color2('green');
const toString2 = green2.toString;
assert.equal(
toString2(), 'Color(green)'
);

// The own property green2.toString is different
// from Color2.prototype.toString
assert.ok(Object.hasOwn(green2, 'toString'));
assert.notEqual(
green2.toString,
Color2.prototype.toString
);

例子: 将函数应用于方法

import { memoize } from 'lodash-es';

function applyFunction(functionFactory) {
return (value, {kind}) => { // decorator function
if (kind === 'method') {
return functionFactory(value);
}
};
}

let invocationCount = 0;

class Task {
@applyFunction(memoize)
expensiveOperation(str) {
invocationCount++;
// Expensive processing of `str` 😀
return str + str;
}
}

const task = new Task();
assert.equal(
task.expensiveOperation('abc'),
'abcabc'
);
assert.equal(
task.expensiveOperation('abc'),
'abcabc'
);
assert.equal(
invocationCount, 1
);

类的getter装饰器/setter装饰器

type ClassGetterDecorator = (
value: Function,
context: {
kind: 'getter';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;

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

getter和setter装饰器和方法装饰器的能力是类似的.

例子: 延迟计算值

  • 实现一个延迟计算值的属性, 我们通过:
    • 通过getter实现该属性, 这样计算值的代码会在读取属性的时候执行
    • 使用装饰器@lazy包装原始的getter
class C {
@lazy
get value() {
console.log('COMPUTING');
return 'Result of computation';
}
}

function lazy(value, {kind, name, addInitializer}) {
if (kind === 'getter') {
return function () {
const result = value.call(this);
Object.defineProperty( // (A)
this, name,
{
value: result,
writable: false,
}
);
return result;
};
}
}

类字段装饰器

type ClassFieldDecorator = (
value: undefined,
context: {
kind: 'field';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown, set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => (initialValue: unknown) => unknown | void;
  • 它不能更改或者替换字段(需要自动访问器来实现)
  • 可以通过返回原始的初始化值兵返回新的初始化值的函数来更改初始化值
  • 可以注册初始设定项
  • 可以通过公开其字段的访问(即便它是私有的) context.access

例子: 更改字段的初始化值

function twice() {
return initialValue => initialValue * 2;
}

class C {
@twice
field = 3;
}

const inst = new C();
assert.equal(
inst.field, 6
);

例子: 只读字段

const readOnlyFieldKeys = Symbol('readOnlyFieldKeys');

@readOnly
class Color {
@readOnly
name;
constructor(name) {
this.name = name;
}
}

const blue = new Color('blue');
assert.equal(blue.name, 'blue');
assert.throws(
() => blue.name = 'brown',
/^TypeError: Cannot assign to read only property 'name'/
);

function readOnly(value, {kind, name}) {
if (kind === 'field') { // (A)
return function () {
if (!this[readOnlyFieldKeys]) {
this[readOnlyFieldKeys] = [];
}
this[readOnlyFieldKeys].push(name);
};
}
if (kind === 'class') { // (B)
return function (...args) {
const inst = new value(...args);
for (const key of inst[readOnlyFieldKeys]) {
Object.defineProperty(inst, key, {writable: false});
}
return inst;
}
}
}
  1. 首先收集只读字段的所有键(A)
  2. 等实例完全设置完成, 并使我们收集齐密钥的字段不可写(B)

例子: 依赖注入, 实例公共字段.

const {registry, inject} = createRegistry();

class Logger {
log(str) {
console.log(str);
}
}
class Main {
@inject logger;
run() {
this.logger.log('Hello!');
}
}

registry.register('logger', Logger);
new Main().run();

// Output:
// Hello!

其中createRegistry实现如下:

function createRegistry() {
const nameToClass = new Map();
const nameToInstance = new Map();
const registry = {
register(name, componentClass) {
nameToClass.set(name, componentClass);
},
getInstance(name) {
if (nameToInstance.has(name)) {
return nameToInstance.get(name);
}
const componentClass = nameToClass.get(name);
if (componentClass === undefined) {
throw new Error('Unknown component name: ' + name);
}
const inst = new componentClass();
nameToInstance.set(name, inst);
return inst;
},
};
function inject (_value, {kind, name}) {
if (kind === 'field') {
return () => registry.getInstance(name);
}
}
return {registry, inject};
}

例子: "朋友"可见性

我们可以通过将类的字段设置为私有来更改某些类成员的可见性. 以防它们被公开访问.

朋友可见性指的是允许一些"朋友"来访问一些私有成员类型.

比如:

const friendName = new Friend();

class ClassWithSecret {
@friendName.install #name = 'Rumpelstiltskin';
getName() {
return this.#name;
}
}

// Everyone who has access to `secret`, can access inst.#name
const inst = new ClassWithSecret();
assert.equal(
friendName.get(inst), 'Rumpelstiltskin'
);
friendName.set(inst, 'Joe');
assert.equal(
inst.getName(), 'Joe'
);

其中Friend的实现如下:

class Friend {
#access = undefined;
#getAccessOrThrow() {
if (this.#access === undefined) {
throw new Error('The friend decorator wasn’t used yet');
}
return this.#access;
}
// An instance property whose value is a function whose `this`
// is fixed (bound to the instance).
install = (_value, {kind, access}) => {
if (kind === 'field') {
if (this.#access) {
throw new Error('This decorator can only be used once');
}
this.#access = access;
}
}
get(inst) {
return this.#getAccessOrThrow().get.call(inst);
}
set(inst, value) {
return this.#getAccessOrThrow().set.call(inst, value);
}
}

例子: 枚举

有很多方法可以实现枚举, OOP风格的方法是使用类和静态属性:

class Color {
static red = new Color('red');
static green = new Color('green');
static blue = new Color('blue');
constructor(enumKey) {
this.enumKey = enumKey;
}
toString() {
return `Color(${this.enumKey})`;
}
}
assert.equal(
Color.green.toString(),
'Color(green)'
);

我们可以使用装饰器来自动的:

  • 从枚举键创建一个映射到枚举值
  • 将枚举键添加到枚举值--无需将它们传递给构造函数

类似这样:

function enumEntry(value, {kind, name}) {
if (kind === 'field') {
return function (initialValue) {
if (!Object.hasOwn(this, 'enumFields')) {
this.enumFields = new Map();
}
this.enumFields.set(name, initialValue);
initialValue.enumKey = name;
return initialValue;
};
}
}

class Color {
@enumEntry static red = new Color();
@enumEntry static green = new Color();
@enumEntry static blue = new Color();
toString() {
return `Color(${this.enumKey})`;
}
}
assert.equal(
Color.green.toString(),
'Color(green)'
);
assert.deepEqual(
Color.enumFields,
new Map([
['red', Color.red],
['green', Color.green],
['blue', Color.blue],
])
);

自动访问器

装饰器提案引入了一个新的语言特性: 自动访问器

通过将关键词放在accessor类字段之前来创建自动访问器. 它想字段一样使用, 但是运行时有不同的方式来实现:

class C {
static accessor myField1;
static accessor #myField2;
accessor myField3;
accessor #myField4;
}

字段和自动访问器有什么不同?

  • 一个字段创建:
    • 属性
    • 专用插槽
  • 自动访问器为数据创建一个私有槽, 并且:
    • 公共getter-setter
    • 私有getter-setter对: 私有插槽不会被继承, 所以永远不会位于原型中.

比如下面这个类:

class C {
accessor str = 'abc';
}
const inst = new C();

assert.equal(
inst.str, 'abc'
);
inst.str = 'def';
assert.equal(
inst.str, 'def'
);

它在内部看起来是这样的:

class C {
#str = 'abc';
get str() {
return this.#str;
}
set str(value) {
this.#str = value;
}
}

为什么需要

  1. 他们只影响初始化的值字段
  2. 他们可以完全取代自动存取器

每当装饰器需要比字段更多的控制能力时, 我们就必须使用自动访问器, 而不是字段

类自动访问装饰器

type ClassAutoAccessorDecorator = (
value: {
get: () => unknown;
set: (value: unknown) => void;
},
context: {
kind: 'accessor';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown, set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => {
get?: () => unknown;
set?: (value: unknown) => void;
init?: (initialValue: unknown) => unknown;
} | void;
  • 它能通过气参数接受自动访问器的gettersetter
  • 它可以通过发挥带有方法.get()来替换修饰的自动访问器.set()
  • 它可以通过使用方法返回一个对象来影响自动访问器的初始值
  • 它可以注册初始值设定项.

例子: 只读自动访问器

const UNINITIALIZED = Symbol('UNINITIALIZED');
function readOnly({get,set}, {name, kind}) {
if (kind === 'accessor') {
return {
init() {
return UNINITIALIZED;
},
get() {
const value = get.call(this);
if (value === UNINITIALIZED) {
throw new TypeError(
`Accessor ${name} hasn’t been initialized yet`
);
}
return value;
},
set(newValue) {
const oldValue = get.call(this);
if (oldValue !== UNINITIALIZED) {
throw new TypeError(
`Accessor ${name} can only be set once`
);
}
set.call(this, newValue);
},
};
}
}

class Color {
@readOnly
accessor name;
constructor(name) {
this.name = name;
}
}

const blue = new Color('blue');
assert.equal(blue.name, 'blue');
assert.throws(
() => blue.name = 'yellow',
/^TypeError: Accessor name can only be set once$/
);

const orange = new Color('orange');
assert.equal(orange.name, 'orange');

参考