装饰器(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
,变成输出A
和Z
)。此类别D
就是一个装饰器,它的用法如下:
let d = new D( new C());
dA();
TypeScript的装饰器
TypeScript内建装饰器的支援,可以用比较易读的语法来套用装饰器设计模式。不过这个功能还在实验中,如果要使用,必须要在tsconfig.json
档案中的compilerOptions
栏位内,加上experimentalDecorators
栏位,并将栏位值设为true
。
装饰器
TypeScript的装饰器为一个函数,这个函数的型别会根据其要装饰的对象类型而有所不同。以下会依照装饰器的装饰对象来介绍装饰器。
类别装饰器
类别装饰器的函数型别为(target: any) => void
,target
参数即为装饰对象的建构子函数(即类别本身)。
例如以下函数,就是一个类别装饰器:
function d ( target: any ) {
console .log( 'Class Decorator' );
}
我们可以用小老鼠符号@
再接上装饰器名称(即函数名称或储存函数的变数、参数、常数的名称)或是路径,来套用该装饰器。
类别装饰器的@
要放置于class
关键字之前。如果也有使用export
关键字,要放在export
关键字之前。
例如:
|
function d ( target: any ) { |
|
console .log( 'Class Decorator' ); |
|
} |
|
|
|
|
|
class C { |
|
|
|
} |
编译以上程序并立刻执行的话,会输出Class Decorator
文字。
方法装饰器
若TypeScript的编译目标为ES5
或是以上的版本,方法装饰器的函数型别为(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void
或(target: any, propertyKey: string) => void
。target
参数储存要用来装饰的类别建构子(装饰静态方法时),或是该类别的原型(装饰物件方法时)。propertyKey
参数储存要用来装饰的方法名称,descriptor
参数则是储存属性的「选项」(例如在先前章节有稍微提到的enumerable
)。
例如以下函数,就是一个方法装饰器:
function d ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) {
console .log( 'Method Decorator' );
}
若TypeScript的编译目标为ES3
,则方法装饰器的函数型别只能为(target: any, propertyKey: string) => void
。
方法装饰器的@
要放置于方法名称之前。如果也有使用private
、protected
、set
、get
关键字,要放在它们之前。
例如:
|
function d ( target: any , propertyKey: string , descriptor: PropertyDescriptor ) { |
|
console .log( 'Method Decorator' ); |
|
} |
|
|
|
class C { |
|
|
|
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 { |
|
|
|
A() { |
|
console .log( 'A' ); |
|
} |
|
|
|
B() { |
|
console .log( 'B' ); |
|
} |
|
} |
这边要注意的是,若要在descriptor
参数的value
栏位中呼叫原本的方法,最好用方法提供的call
函数并代入this
作为参数来呼叫,这样才可以确保在原先方法中使用到的this
关键字会是该物件本身的参考。
参数装饰器
参数装饰器的函数型别为(target: any, propertyKey: string, parameterIndex: number) => void
。target
参数储存要用来装饰的类别建构子(装饰静态方法的参数或是建构子的参数时),或是该类别的原型(装饰物件方法的参数时)。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 ( public a: number ) { |
|
|
|
} |
|
|
|
m( number ) { | a:
|
this .a = a; |
|
} |
|
} |
编译以上程序并立刻执行的话,会输出两行Parameter Decorator
文字。
栏位装饰器
栏位装饰器的函数型别为(target: any, propertyKey: string) => void
。target
参数储存要用来装饰的类别建构子(装饰静态栏位时),或是该类别的原型(装饰物件栏位时)。propertyKey
参数储存要用来装饰的栏位名称。
例如以下函数,就是一个栏位装饰器:
function d ( target: any , propertyKey: string ) {
console .log( 'Field Decorator' );
}
栏位装饰器的@
要放置于栏位名称之前。如果也有使用private
、protected
、readonly
关键字,要放在它们之前。
例如:
function d ( target: any , propertyKey: string ) { |
console .log( 'Field Decorator' ); |
} |
class C { |
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' ); |
|
}; |
|
} |
|
|
|
() |
|
class C { |
|
|
|
} |
编译以上程序并立刻执行的话,会输出Class Decorator Factory
和Class 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 { |
|
'm1' ) | (
|
m1() { |
|
|
|
} |
|
|
|
'm2' ) | (
|
m2() { |
|
|
|
} |
|
|
|
'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' ); |
|
} |
|
|
|
|
|
class A { |
|
|
|
|
|
m = 0 ; |
|
|
|
n = 0 ; |
|
} |
|
|
|
|
|
class B { |
|
|
|
|
|
run() { |
|
|
|
} |
|
|
|
|
|
f = 0 ; |
|
|
|
|
|
compute( number , y: number ) { | x:
|
|
|
} |
|
} |
以上程序的输出结果如下:
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' ; |
|
|
|
'k1' , '123' ) | .metadata(
|
class C { |
|
'k2' , '456' ) | .metadata(
|
n: number = 0 ; |
|
|
|
'k3' , 789 ) | .metadata(
|
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); |
|
}; |
|
} |
|
|
|
'k1' , '123' ) | (
|
class C { |
|
'k2' , '456' ) | (
|
n: number = 0 ; |
|
|
|
'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 { |
|
'/a' ) | (
|
'/a' ) | (
|
someGetMethod() { |
|
return 'Hello, world!' ; |
|
} |
|
|
|
'/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程序的利器!