目录javascript
修饰器是ES7
加入的新特性,Angular
中进行了大量使用,有不少内置的修饰器,后端的同窗通常称之为“注解”。修饰器的做用,实际上就是设计模式中常说的装饰者模式的一种实现,早在ES6
开始,设计模式原生化就已是很是明显的趋势了,不管是for..of..
和Iterator
接口的配合内化了迭代者模式,Proxy
对象实现的代理模式等等,均可以看出Javascript
逐渐走向标准化的趋势和决心。css
装饰者模式,是指在没必要改变原类文件或使用继承的状况下,动态地扩展一个对象的功能,为对象增长额外特性的一种设计模式。考虑到javascript
中函数参数为对象时只传递地址这一特性,装饰者模式其实是很是好复现的,掌握其基本知识对于理解Angular
技术栈的原理和执行流程是必不可少的,从结果的角度来看,使用装饰器和直接修改类的定义没有什么区别,但使用装饰器更符合开放封闭原则,且更符合声明式的思想,本文着重分析Typescript
中支持的几种不一样的装饰器用法。html
类装饰器,就是用来装饰类的,它只接受一个参数,就是被装饰的类。下面的示例使用@testable
修饰器为已定义的类加上一个__testable
属性:java
//装饰器修改的是类定义的表现,故在javascript中模拟时须要直接将变化添加至原型上 function testable(target: Function):void{ target.prototype.__testable = false; } //使用类装饰器 @testable class Person{ constructor(){} } //测试装饰后的结果 let person = new Person(); console.log(person.__testable);//false
另外一方面,咱们可使用工厂函数的方法生成一个可接收附加参数的装饰器,借助高阶函数的思路不难理解,例如Angular
中常见的这种形式:node
//Angular中的组件定义 @Component({ selector:'hero-detail', templateUrl:'hero-detail.html', styleUrls:['style.css'] }) export Class MyComponent{ constructor(){} } //@Component装饰者类的做用机制能够理解为: function Component(params:any){ return function(target: Function):void{ target.prototype.metadata = params; } }
这样在组件被实例化时,就能够获取到传入的元数据信息。换句话说,Component({...})
执行后返回的函数才是真正的类装饰器,Component
是一个接受参数而后生成装饰器的函数,也就是装饰器工厂,从元编程的角度来说,至关于修改了new
操做符的行为。typescript
方法修饰器声明在一个方法的声明以前,会被应用到方法的属性描述符上,能够用来检视,修改或者替换方法定义。它接收以下三个参数:编程
下面的装饰器@enumerable
将被修饰对象修改成可枚举:后端
//方法装饰器,返回值会直接赋值给方法的属性描述符。 function enumerable(target: any, propertyKey: string, descriptor:PropertyDescriptor):void{ descriptor.enumerable = true; } class Person{ constructor(){} @enumerable//使用方法装饰器 sayHi(){ console.log('Hi'); } } //测试装饰后的结果 let person = new Person(); console.log(person.__testable);//false
更经常使用的方式依然是利用高阶函数返回一个可被外部控制的装饰器:设计模式
function enumerable(value: boolean){ return function (target: any, propertyKey: string, descriptor:PropertyDescriptor):void{ descriptor.enumerable = true; } }
访问器,通常指属性的get/set
方法,和普通方法装饰器用法一致,须要注意的是typescript中不支持同时装饰一个成员的get
访问器和set
访问器。数组
属性装饰器表达式运行时接收两个参数:
Typescript官方文档给出的示例是这样的:
class Greeter { @format("Hello, %s") greeting: string; constructor(message: string){ this.greeting = message; } greet(){ let formatString = getFormat(this, 'greeting'); return formatString.replace('s%',this.greeting); } }
而后定义@format
装饰器和getFormat
函数:
.import "reflect-metadata"; const formatMetadataKey = Symbol("format"); function format(formatString: string) { return Reflect.metadata(formatMetadataKey, formatString); } function getFormat(target: any, propertyKey: string) { return Reflect.getMetadata(formatMetadataKey, target, propertyKey); }
与方法装饰器相比,属性装饰器的形参列表中并无属性描述符,由于目前没有办法在定义一个原型对象的成员时描述一个实例属性,也没法监视属性的初始化方法。TS中的属性描述符单独使用时只能用来监视类中是否声明了某个名字的属性,示例中经过外部功能扩展了其实用性。Angular中最多见的属性修饰器就是Input( )
和output( )
。
参数装饰器通常用于装饰参数,在类构造函数或方法声明中装饰形参。
它在运行时被当作函数调用,传入下列3个参数:
TS中参数装饰器单独使用时只能用来监视一个方法的参数是否被传入,Typescript官方给出的示例以下:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @validate greet(@required name: string) {//此处使用了参数修饰符 return "Hello " + name + ", " + this.greeting; } }
两个装饰器的定义以下:
import "reflect-metadata"; const requiredMetadataKey = Symbol('required'); /* *@required参数装饰器 *实现的功能就是当函数的参数必须填入时,将相关信息存储到一个外部的数组中,能够看出参数装饰器并*未对参数自己作出什么修改。 */ function required(target: Object, propertyKey:string | symbol, parameterIndex: number){ let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey); } /* *@validate装饰器为方法装饰器 *展现了如何经过操做方法属性描述符中的value属性来实现方法的代理访问。 */ function validate(target:any, propertyName: string, descriptor:TypedPropertyDescriptor<Function>){ let method = descriptor.value;//方法的属性修饰符的value就是方法的函数表达式 descriptor.value = function(){ let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);//在外部存储中查找是否有必填参数 if (requiredParameters){ for(let parameterIndex of requiredParameters){ if(parameterIndex >= arguments.length || arguments[parameterIndex] === undefined){ //传入参数不足或被约束参数为undefined时抛出错误。 throw new Error('Missing required argument'); } } } return method.apply(this, arguments);//若是没有任何错误抛出则继续执行原函数 } }
在Typescript中,装饰器的运行顺序基本依照参数装饰器,方法装饰器,访问符装饰器,属性装饰器,类装饰器这样的顺序来运行,因此参数装饰器和方法装饰器能够联合使用实现一些额外功能。
用ES5
来模拟一下上述的方法装饰器和参数装饰器联合做用的例子,就很容易看出装饰器的做用:
//使用ES5语法模拟装饰器 function Greeter(message){ this.greeting = message; } Greeter.prototype.greet = function(name){ return "Hello " + name + ", " + this.greeting; } //外部存储的必要性校验 requiredArray = {}; //参数装饰器 function requireDecorator(FnKey,paramsIndex){ requiredArray[FnKey] = paramsIndex; } //装饰器函数 function validateDecorator(Fn,FnKey){ let method = Fn; return function(){ let checkParamIndex = requiredArray[FnKey]; if(checkParamIndex > arguments.length-1 || arguments[checkParamIndex] === undefined){ throw new Error('params invalid'); } return method.apply(this, arguments); } } //运行装饰 requireDecorator('greet',0); Greeter.prototype.greet = validateDecorator(Greeter.prototype.greet, 'greet'); //测试装饰 let greeter = new Greeter('welcome to join the conference'); console.log(greeter.greet('Tony')); console.log(greeter.greet());
在node环境中运行一下就能够看到,greet( )
方法在未传入参数时会报错提示。
装饰器实际上就是一种更加简洁的代码书写方式,从代码表现来理解,就是使用闭包和高阶函数扩展或者修改了原来的表现,从功能角度来理解,达到了不修改内部实现的前提下动态扩展和修改类定义的目的。