【Angular專題】 (3)裝飾器decorator,一塊語法糖

一. Decorator裝飾器

修飾器是ES7加入的新特性,Angular中進行了大量使用,有不少內置的修飾器,後端的同窗通常稱之爲「註解」。修飾器的做用,實際上就是設計模式中常說的裝飾者模式的一種實現,早在ES6開始,設計模式原生化就已是很是明顯的趨勢了,不管是for..of..Iterator接口的配合內化了迭代者模式Proxy對象實現的代理模式等等,均可以看出Javascript逐漸走向標準化的趨勢和決心。css

裝飾者模式,是指在沒必要改變原類文件或使用繼承的狀況下,動態地擴展一個對象的功能,爲對象增長額外特性的一種設計模式。考慮到javascript中函數參數爲對象時只傳遞地址這一特性,裝飾者模式其實是很是好復現的,掌握其基本知識對於理解Angular技術棧的原理和執行流程是必不可少的,從結果的角度來看,使用裝飾器和直接修改類的定義沒有什麼區別,但使用裝飾器更符合開放封閉原則,且更符合聲明式的思想,本文着重分析Typescript中支持的幾種不一樣的裝飾器用法。html

二. Typescript中的裝飾器

2.1 類裝飾器

類裝飾器,就是用來裝飾類的,它只接受一個參數,就是被裝飾的類。下面的示例使用@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

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( )方法在未傳入參數時會報錯提示。

四. 小結

裝飾器實際上就是一種更加簡潔的代碼書寫方式,從代碼表現來理解,就是使用閉包和高階函數擴展或者修改了原來的表現,從功能角度來理解,達到了不修改內部實現的前提下動態擴展和修改類定義的目的。

相關文章
相關標籤/搜索