14. 装饰器

TS系列332835 阅读0

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。

在语法上,装饰器有如下几个特征。

(1)第一个字符(或者说前缀)是@,后面是一个表达式。

(2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。

(3)这个函数接受所修饰对象的一些相关值作为参数。

(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

举例来说,有一个函数Injectable()当作装饰器使用,那么需要写成@Injectable,然后放在某个类的前面。

@Injectable class A {
// ...
}

上面示例中,由于有了装饰器@Injectable,类A的行为在运行时就会发生改变。类A在执行前会先执行装饰器Injectable(),并且会向装饰器自动传入参数就可以了。

相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。

装饰器函数的类型定义如下。

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

上面代码中,Decorator是装饰器的类型定义。它是一个函数,使用时会接收到value和context两个参数。

value:所装饰的对象。

context:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext,描述这个对象。

function decorator(
value:any,
context:ClassMethodDecoratorContext
) {
// ...
}

上面是一个装饰器函数,其中第二个参数context的类型就可以写成ClassMethodDecoratorContext。

context对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kind和name)是必有的,其他都是可选的。

(1)kind:字符串,表示所装饰对象的类型,可能取以下的值。

'class'

'method'

'getter'

'setter'

'field'

'accessor'

这表示一共有六种类型的装饰器。

(2)name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。

(3)addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值。

(4)private:布尔值,表示所装饰的对象是否为类的私有成员。

(5)static:布尔值,表示所装饰的对象是否为类的静态成员。

(6)access:一个对象,包含了某个值的 get 和 set 方法。

类装饰器

类装饰器接受两个参数:value(当前类本身)和context(上下文对象)。其中,context对象的kind属性固定为字符串class。

类装饰器一般用来对类进行操作,可以不返回任何值,请看下面的例子。

function Greeter(value, context) {
if (context.kind === 'class') {
value.prototype.greet = function () {
console.log('你好');
};
}
}

@Greeter
class User {}

let u = new User();
u.greet(); // "你好"

上面示例中,类装饰器@Greeter在类User的原型对象上,添加了一个greet()方法,实例就可以直接使用该方法。

类装饰器可以返回一个函数,替代当前类的构造方法。


function countInstances(value:any, context:any) {
let instanceCount = 0;

const wrapper = function (...args:any[]) {
instanceCount++;
const instance = new value(...args);
instance.count = instanceCount;
return instance;
} as unknown as typeof MyClass;

wrapper.prototype = value.prototype; // A
return wrapper;
}

@countInstances
class MyClass {}

const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1

上面示例中,类装饰器@countInstances返回一个函数,替换了类MyClass的构造方法。新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且对实例添加count属性,表示当前实例的编号。(自然也能用这个方法来创建单例模式

注意,上例为了确保新构造方法继承定义在MyClass的原型之上的成员,特别加入A行,确保两者的原型对象是一致的。否则,新的构造函数wrapper的原型对象,与MyClass不同,通不过instanceof运算符。

类装饰器也可以返回一个新的类,替代原来所装饰的类。

function countInstances(value:any, context:any) {
let instanceCount = 0;

return class extends value {
constructor(...args:any[]) {
super(...args);
instanceCount++;
this.count = instanceCount;
}
};
}

@countInstances
class MyClass {}

const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1

上面示例中,@countInstances返回一个MyClass的子类。

下面的例子是通过类装饰器,禁止使用new命令新建类的实例。


function functionCallable(
value:any, {kind}:any
):any {
if (kind === 'class') {
return function (...args:any) {
if (new.target !== undefined) {
throw new TypeError('This function can’t be new-invoked');
}
return new value(...args);
}
}
}

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

// @ts-ignore
const robin = Person('Robin');
robin.name // 'Robin'

上面示例中,类装饰器@functionCallable返回一个新的构造方法,里面判断new.target是否不为空,如果是的,就表示通过new命令调用,从而报错。

方法装饰器

方法装饰器用来装饰类的方法(method)。它的类型描述如下。

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

根据上面的类型,方法装饰器是一个函数,接受两个参数:value和context。

参数value是方法本身,参数context是上下文对象,有以下属性。

kind:值固定为字符串method,表示当前为方法装饰器。

name:所装饰的方法名,类型为字符串或 Symbol 值。

static:布尔值,表示是否为静态方法。该属性为只读属性。

private:布尔值,表示是否为私有方法。该属性为只读属性。

access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。

addInitializer():为方法增加初始化函数。

方法装饰器会改写类的原始方法,实质等同于下面的操作。

function trace(decoratedMethod) {
// ...
}

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

// `@trace` 等同于
// C.prototype.toString = trace(C.prototype.toString);

上面示例中,@trace是方法toString()的装饰器,它的效果等同于最后一行对toString()的改写。

如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。

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

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

@replaceMethod
hello() {
return `Hi ${this.name}!`;
}
}

const robin = new Person('Robin');

robin.hello() // 'How are you, Robin?'

上面示例中,装饰器@replaceMethod返回的函数,就成为了新的hello()方法。

利用方法装饰器,可以将类的方法变成延迟执行。

function delay(milliseconds: number = 0) {
return function (value, context) {
if (context.kind === "method") {
return function (...args: any[]) {
setTimeout(() => {
value.apply(this, args);
}, milliseconds);
};
}
};
}

class Logger {
@delay(1000)
log(msg: string) {
console.log(`${msg}`);
}
}

let logger = new Logger();
logger.log("Hello World");

上面示例中,方法装饰器@delay(1000)将方法log()的执行推迟了1秒(1000毫秒)。这里真正的方法装饰器,是delay()执行后返回的函数,delay()的作用是接收参数,用来设置推迟执行的时间。这种通过高阶函数返回装饰器的做法,称为“工厂模式”,即可以像工厂那样生产出一个模子的装饰器。

方法装饰器的参数context对象里面,有一个addInitializer()方法。它是一个钩子方法,用来在类的初始化阶段,添加回调函数。这个回调函数就是作为addInitializer()的参数传入的,它会在构造方法执行期间执行,早于属性(field)的初始化。

属性装饰器

属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。

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;

注意,装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field,而不是“property”或“attribute”,这一点是需要注意的。

属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。


function logged(value, context) {
const { kind, name } = context;
if (kind === 'field') {
return function (initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return initialValue;
};
}
}

class Color {
@logged name = 'green';
}

const color = new Color();
// "initializing name with value green"

上面示例中,属性装饰器@logged装饰属性name。@logged的返回值是一个函数,该函数用来对属性name进行初始化,它的参数initialValue就是属性name的初始值green。新建实例对象color时,该函数会自动执行。

属性装饰器的返回值函数,可以用来更改属性的初始值。


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

class C {
@twice
field = 3;
}

const inst = new C();
inst.field // 6

上面示例中,属性装饰器@twice返回一个函数,该函数的返回值是属性field的初始值乘以2,所以属性field的最终值是6。

getter 装饰器,setter 装饰器

getter 装饰器和 setter 装饰器,是分别针对类的取值器(getter)和存值器(setter)的装饰器。

注意,getter 装饰器的上下文对象context的access属性,只包含get()方法;setter 装饰器的access属性,只包含set()方法。

这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。

accessor 装饰器

装饰器语法引入了一个新的属性修饰符accessor。

class C {
accessor x = 1;
}

上面示例中,accessor修饰符等同于为公开属性x自动生成取值器和存值器,它们作用于私有属性x。(注意,公开的x与私有的x不是同一个属性。)也就是说,上面的代码等同于下面的代码。

class C {
#x = 1;

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

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

评论

发表评论