装饰器(Decorator)是一种程序设计模式(Design Pattern),在某些情况下可以用来替代继承(inheritance),以更灵活、轻量的方式重用程序码。

装饰器其实就是Wrapper的一种应用,举例来说:


	
 
class C {
 
A() {
 
console .log( 'A' );
 
}
 
 
 
B() {
 
console .log( 'B' );
 
}
 
}
 
 
 
class D {
 
c: C;
 
 
 
constructor ( c: C ) {
 
this .c = c;
 
}
 
 
 
A() {
 
this .cA();
 
 
 
console .log( 'Z' );
 
}
 
}

以上程序中,D的物件实体可以包裹一个C的物件实体,并且扩充C的物件实体的A方法的功能(原本只输出A,变成输出AZ)。此类别D就是一个装饰器,它的用法如下:

let d = new D( new C());

dA();

TypeScript的装饰器

TypeScript内建装饰器的支援,可以用比较易读的语法来套用装饰器设计模式。不过这个功能还在实验中,如果要使用,必须要在tsconfig.json档案中的compilerOptions栏位内,加上experimentalDecorators栏位,并将栏位值设为true

装饰器

TypeScript的装饰器为一个函数,这个函数的型别会根据其要装饰的对象类型而有所不同。以下会依照装饰器的装饰对象来介绍装饰器。

类别装饰器

类别装饰器的函数型别为(target: any) => voidtarget参数即为装饰对象的建构子函数(即类别本身)。

例如以下函数,就是一个类别装饰器:

function  d ( target: any ) {
     console .log( 'Class Decorator' );
}

我们可以用小老鼠符号@再接上装饰器名称(即函数名称或储存函数的变数、参数、常数的名称)或是路径,来套用该装饰器。

类别装饰器的@要放置于class关键字之前。如果也有使用export关键字,要放在export关键字之前。

例如:


	
 
function d ( target: any ) {
 
console .log( 'Class Decorator' );
 
}
 
 
 
@d
 
class C {
 
 
 
}

编译以上程序并立刻执行的话,会输出Class Decorator文字。

方法装饰器

若TypeScript的编译目标为ES5或是以上的版本,方法装饰器的函数型别为(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void(target: any, propertyKey: string) => voidtarget参数储存要用来装饰的类别建构子(装饰静态方法时),或是该类别的原型(装饰物件方法时)。propertyKey参数储存要用来装饰的方法名称,descriptor参数则是储存属性的「选项」(例如在先前章节有稍微提到的enumerable)。

例如以下函数,就是一个方法装饰器:

function  d ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
     console .log( 'Method Decorator' );
}

若TypeScript的编译目标为ES3,则方法装饰器的函数型别只能为(target: any, propertyKey: string) => void

方法装饰器的@要放置于方法名称之前。如果也有使用privateprotectedsetget关键字,要放在它们之前。

例如:


	
 
function d ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
 
console .log( 'Method Decorator' );
 
}
 
 
 
class C {
 
@d
 
m() {
 
 
 
}
 
}

编译以上程序并立刻执行的话,会输出Method Decorator文字。

descriptor参数的value栏位即为方法本身。例如上面看过的这个例子:


	
 
class C {
 
A() {
 
console .log( 'A' );
 
}
 
 
 
B() {
 
console .log( 'B' );
 
}
 
}
 
 
 
class D {
 
c: C;
 
 
 
constructor ( c: C ) {
 
this .c = c;
 
}
 
 
 
A() {
 
this .cA();
 
 
 
console .log( 'Z' );
 
}
 
}

可以将「方法执行完之后输出一个Z文字」的功能做成TypeScript的装饰器,将程序改写如下:


	
 
function Z ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
 
let originalMethod = descriptor.value;
 
 
 
descriptor.value = function () {
 
originalMethod.call( this );
 
 
 
console .log( 'Z' );
 
};
 
}
 
 
 
class C {
 
@Z
 
A() {
 
console .log( 'A' );
 
}
 
 
 
B() {
 
console .log( 'B' );
 
}
 
}

这边要注意的是,若要在descriptor参数的value栏位中呼叫原本的方法,最好用方法提供的call函数并代入this作为参数来呼叫,这样才可以确保在原先方法中使用到的this关键字会是该物件本身的参考。

参数装饰器

参数装饰器的函数型别为(target: any, propertyKey: string, parameterIndex: number) => voidtarget参数储存要用来装饰的类别建构子(装饰静态方法的参数或是建构子的参数时),或是该类别的原型(装饰物件方法的参数时)。propertyKey参数储存要用来装饰的参数所在的方法名称。parameterIndex储存要用来装饰的参数序数(从0开始数)。

例如以下函数,就是一个参数装饰器:

function  d ( target: any , propertyKey: string , parameterIndex: number ) {
     console .log( 'Parameter Decorator' );
}

参数装饰器的@要放置于参数名称之前。如果也有使用public关键字,要放在它之前。

例如:


	
 
function d ( target: any , propertyKey: string , parameterIndex: number ) {
 
console .log( 'Parameter Decorator' );
 
}
 
 
 
class C {
 
constructor ( @d public a: number ) {
 
 
 
}
 
 
 
m( @d a: number ) {
 
this .a = a;
 
}
 
}

编译以上程序并立刻执行的话,会输出两行Parameter Decorator文字。

栏位装饰器

栏位装饰器的函数型别为(target: any, propertyKey: string) => voidtarget参数储存要用来装饰的类别建构子(装饰静态栏位时),或是该类别的原型(装饰物件栏位时)。propertyKey参数储存要用来装饰的栏位名称。

例如以下函数,就是一个栏位装饰器:

function  d ( target: any , propertyKey: string ) {
     console .log( 'Field Decorator' );
}

栏位装饰器的@要放置于栏位名称之前。如果也有使用privateprotectedreadonly关键字,要放在它们之前。

例如:


	
function d ( target: any , propertyKey: string ) {
console .log( 'Field Decorator' );
}
 
class C {
@d
n: number = 0 ;
}

编译以上程序并立刻执行的话,会输出Field Decorator文字。

装饰器工厂(Decorator Factory)

我们可以再撰写一层拥有自订参数的函数,用它来产生装饰器,这个函数即称为「装饰器工厂」。装饰器工厂函数的型别并没有限制,但它的回传值型别必须要是上面提到的那些装饰器的型别。

例如以下函数,就是一个类别装饰器工厂:

function  f () {
     console .log( 'Class Decorator Factory' );

    return  ( target: any ) => {
         console .log( 'Class Decorator' );
    };
}

我们可以用小老鼠符号@接上装饰器工厂名称或是路径,再用小括号()传入要代进装饰器工厂的参数,来套用该装饰器工厂。

例如:


	
 
function f () {
 
console .log( 'Class Decorator Factory' );
 
 
 
return ( target: any ) => {
 
console .log( 'Class Decorator' );
 
};
 
}
 
 
 
@f ()
 
class C {
 
 
 
}

编译以上程序并立刻执行的话,会输出Class Decorator FactoryClass Decorator文字。

再举个例子,利用装饰器工厂,我们可以替方法快速加上Log功能,来纪录它们何时被呼叫的。程序代码如下:


	
 
function log ( tag: string ) {
 
return ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) => {
 
let originalMethod = descriptor.value;
 
 
 
descriptor.value = function () {
 
console .log(tag);
 
 
 
originalMethod.call( this );
 
};
 
};
 
}
 
 
 
 
 
class C {
 
@log ( 'm1' )
 
m1() {
 
 
 
}
 
 
 
@log ( 'm2' )
 
m2() {
 
 
 
}
 
 
 
@log ( 'm3' )
 
m3() {
 
 
 
}
 
}

装饰器的顺序

不同对象的装饰器(或装饰器工厂)的套用顺序是从上到下,从内到外,从右到左相同对象的装饰器的套用顺序是从下到上 (或者说从右到左)例如:


	
 
function cd1 ( target: any ) {
 
console .log( 'Class Decorator 1' );
 
}
 
 
 
function cd2 ( target: any ) {
 
console .log( 'Class Decorator 1' );
 
}
 
 
 
function md1 ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
 
console .log( 'Method Decorator 1' );
 
}
 
 
 
function md2 ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
 
console .log( 'Method Decorator 2' );
 
}
 
 
 
function md3 ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
 
console .log( 'Method Decorator 3' );
 
}
 
 
 
function pd1 ( target: any , propertyKey: string , parameterIndex: number ) {
 
console .log( 'Parameter Decorator 1' );
 
}
 
 
 
function pd2 ( target: any , propertyKey: string , parameterIndex: number ) {
 
console .log( 'Parameter Decorator 2' );
 
}
 
 
 
function fd1 ( target: any , propertyKey: string ) {
 
console .log( 'Field Decorator 1' );
 
}
 
 
 
function fd2 ( target: any , propertyKey: string ) {
 
console .log( 'Field Decorator 2' );
 
}
 
 
 
function fd3 ( target: any , propertyKey: string ) {
 
console .log( 'Field Decorator 3' );
 
}
 
 
 
function fd4 ( target: any , propertyKey: string ) {
 
console .log( 'Field Decorator 4' );
 
}
 
 
 
@cd1
 
class A {
 
@fd1
 
@fd2
 
m = 0 ;
 
@fd3
 
n = 0 ;
 
}
 
 
 
@cd2
 
class B {
 
@md1
 
@md2
 
run() {
 
 
 
}
 
 
 
@fd4
 
f = 0 ;
 
 
 
@md3
 
compute( @pd1 x: number , @pd2 y: number ) {
 
 
 
}
 
}

以上程序的输出结果如下:

Field Decorator 2  Field Decorator 1  Field Decorator 3  Class Decorator 1  Method Decorator 2  Method Decorator 1  Field Decorator 4  Parameter Decorator 2  Parameter Decorator 1  Method Decorator 3  Class Decorator 1

元数据

TypeScript的装饰器还可以用来替类别或者类别属性加上元数据(metadata),不过要搭配reflect-metadata套件来使用。

以下指令可以安装reflect-metadata套件:

npm i --save reflect-metadata

用以下方式引用进TypeScript中:


	
 
import 'reflect-metadata' ;

基本用法如下:


	
 
import 'reflect-metadata' ;
 
 
 
@Reflect .metadata( 'k1' , '123' )
 
class C {
 
@Reflect .metadata( 'k2' , '456' )
 
n: number = 0 ;
 
 
 
@Reflect .metadata( 'k3' , 789 )
 
m() {
 
 
 
}
 
}
 
 
 
 
 
console .log(Reflect.getMetadata( 'k1' , C));
 
 
 
let c = new C();
 
 
 
console .log(Reflect.getMetadata( 'k2' , c, 'n' ));
 
console .log(Reflect.getMetadata( 'k3' , c, 'm' ));

透过Reflect.metadata这个装饰器工厂,可以传入元数据的键值和值,它们都可以是任意型别的值。如果要取得元数据,则是透过Reflect.getMetadata方法,第一个参数传入要取得的元数据键值,第二个参数传入元数据的来源(物件)。如果要取得属性的元数据,则第三个参数要传入属性名称。

我们也可以手动建立装饰器,并在其中呼叫Reflect.defineMetadata方法来设定类别或是类别属性的元数据。如下:


	
 
import 'reflect-metadata' ;
 
 
 
function classMetadata ( key: string , value: string | number ) {
 
return ( target: any ) => {
 
Reflect.defineMetadata(key, value, target);
 
};
 
}
 
 
 
function propertyMetadata ( key: string , value: string | number ) {
 
return ( target: any , propertyKey: string ) => {
 
Reflect.defineMetadata(key, value, target, propertyKey);
 
};
 
}
 
 
 
@classMetadata ( 'k1' , '123' )
 
class C {
 
@propertyMetadata ( 'k2' , '456' )
 
n: number = 0 ;
 
 
 
@propertyMetadata ( 'k3' , 789 )
 
m() {
 
 
 
}
 
}
 
 
 
 
 
console .log(Reflect.getMetadata( 'k1' , C));
 
 
 
let c = new C();
 
 
 
console .log(Reflect.getMetadata( 'k2' , c, 'n' ));
 
console .log(Reflect.getMetadata( 'k3' , c, 'm' ));

总结

在这个章节中我们学会了TypeScript的装饰器用法,利用装饰器我们可以省下撰写很多重复程序代码的功夫。最后再举个模拟HTTP路由程序的例子,如下:


	
 
type method = 'get' | 'post' ;
 
 
 
const routes: {
 
[_: string ]: {
 
get ?: Function
 
post?: Function
 
}
 
} = {};
 
 
 
function resolve ( method: method, path: string , ...data: any [] ): any {
 
if (routes.hasOwnProperty(path)) {
 
let handlers = routes[path];
 
 
 
let handler = handlers[method];
 
 
 
if ( typeof handler === 'function' ) {
 
switch (method) {
 
case 'get' :
 
console .log(handler());
 
break ;
 
case 'post' :
 
console .log(handler(...data));
 
break ;
 
}
 
} else {
 
throw Error ( `The path \` ${path} \` has no handler for the \` ${method} \` method.` );
 
}
 
} else {
 
throw Error ( `The path \` ${path} \` is undefined.` );
 
}
 
}
 
 
 
function get ( path: string ) {
 
return ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) => {
 
if (!routes.hasOwnProperty(path)) {
 
routes[path] = {};
 
}
 
 
 
routes[path][ 'get' ] = descriptor.value;
 
}
 
}
 
 
 
function post ( path: string ) {
 
return ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) => {
 
if (!routes.hasOwnProperty(path)) {
 
routes[path] = {};
 
}
 
 
 
routes[path][ 'post' ] = descriptor.value;
 
}
 
}
 
 
 
class RouteClass {
 
@get ( '/a' )
 
@post ( '/a' )
 
someGetMethod() {
 
return 'Hello, world!' ;
 
}
 
 
 
@post ( '/b' )
 
somePostMethod(x: number , y: number ) {
 
return x + y;
 
}
 
}
 
 
 
resolve( 'get' , '/a' );
 
resolve( 'post' , '/a' );
 
resolve( 'post' , '/b' , 1 , 2 );

以上程序的输出结果如下:

Hello, world!  Hello, world!  3

这个系列的文章就到这里为止了,对于习惯使用「静态型别」的程序语言的开发者来说,TypeScript无非是开发「动态型别」的JavaScript程序的利器!