【Angular专题】 (3)装饰器decorator,一块语法糖

一. Decorator装饰器

修饰器是ES7加入的新特性,Angular中进行了大量使用,有很多内置的修饰器,后端的同学一般称之为“注解”。修饰器的作用,实际上就是设计模式中常说的装饰者模式的一种实现,早在ES6开始,设计模式原生化就已经是非常明显的趋势了,无论是for..of..Iterator接口的配合内化了迭代者模式Proxy对象实现的代理模式等等,都可以看出Javascript逐渐走向标准化的趋势和决心。

装饰者模式,是指在不必改变原类文件或使用继承的情况下,动态地扩展一个对象的功能,为对象增加额外特性的一种设计模式。考虑到javascript中函数参数为对象时只传递地址这一特性,装饰者模式实际上是非常好复现的,掌握其基本知识对于理解Angular技术栈的原理和执行流程是必不可少的,从结果的角度来看,使用装饰器和直接修改类的定义没有什么区别,但使用装饰器更符合开放封闭原则,且更符合声明式的思想,本文着重分析Typescript中支持的几种不同的装饰器用法。

二. Typescript中的装饰器

2.1 类装饰器

类装饰器,就是用来装饰类的,它只接受一个参数,就是被装饰的类。下面的示例使用@testable修饰器为已定义的类加上一个__testable属性:

//装饰器修改的是类定义的表现,故在javascript中模拟时需要直接将变化添加至原型上
function testable(target: Function):void{
   target.prototype.__testable = false;
}

//使用类装饰器
@testable
class Person{
   constructor(){}
}

//测试装饰后的结果
let person = new Person();
console.log(person.__testable);//false

另一方面,我们可以使用工厂函数的方法生成一个可接收附加参数的装饰器,借助高阶函数的思路不难理解,例如Angular中常见的这种形式:

//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操作符的行为。

2.2 方法装饰器

方法修饰器声明在一个方法的声明之前,会被应用到方法的属性描述符上,可以用来检视,修改或者替换方法定义。它接收如下三个参数:

  • 1.静态成员时参数是类的构造函数,实例成员时传入类的原型对象。

  • 2.成员名

  • 3.成员属性描述符

下面的装饰器@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;
   }
}

2.3 访问器装饰器

访问器,一般指属性的get/set方法,和普通方法装饰器用法一致,需要注意的是typescript中不支持同时装饰一个成员的get访问器和set访问器。

2.4 属性装饰器

属性装饰器表达式运行时接收两个参数:

  • 1.对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象。

  • 2.成员名

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( )

2.5 参数装饰器

参数装饰器一般用于装饰参数,在类构造函数或方法声明中装饰形参。

它在运行时被当做函数调用,传入下列3个参数:

  • 1.静态成员时接收构造函数,实例成员时接收原型对象。

  • 2.成员名

  • 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来模拟一下上述的方法装饰器和参数装饰器联合作用的例子,就很容易看出装饰器的作用:

//使用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( )方法在未传入参数时会报错提示。

四. 小结

装饰器实际上就是一种更加简洁的代码书写方式,从代码表现来理解,就是使用闭包和高阶函数扩展或者修改了原来的表现,从功能角度来理解,达到了不修改内部实现的前提下动态扩展和修改类定义的目的。

来源:华为云社区  作者:大史不说话

 

 

上一篇:【Angular专题】——(1)Angular,孤傲的变革者


下一篇:【华为云技术分享】三大前端技术(React,Vue,Angular)探密(下)