前端深刻理解TypeScript裝飾器(Decorators)概念

分享一下學習裝飾器(Decorators)的所得express

概念介紹

裝飾器是TypeScript提供的最強大的功能之一,它使咱們可以以乾淨的聲明性方式擴展類和方法的功能。裝飾器目前是JavaScript 的第2階段提議,但在TypeScript生態系統中已受到歡迎,主要的開放源代碼項目(例如Angular)正在使用裝飾器。json

本人工做中是使用Angular8進行項目一個項目的開發的,接觸裝飾器這一方面也就比較多,因此就趁着週末整理了一篇關於裝飾器Decorators的文章,也但願能幫助到學習這方面的同窗們。廢話很少說,下面我們進入正題。c#

開始使用裝飾器Decorators

首先咱們要在tsconfig.json裏面啓用experimentalDecorators編譯器選項:api

命令行:tsc --target ES5 --experimentalDecorators數組

tsconfig.json:bash

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
複製代碼

咱們先明確兩個概念:app

  • 目前裝飾器本質上是一個函數,@expression的形式實際上是一個語法糖,expression求值後必須也是一個函數,它會在運行時被調用,被裝飾的聲明信息作爲參數傳入。
  • JavaScript中的Class其實也是一個語法糖。

例如咱們在Javascript中聲明一個Class:ide

Class Animal {
    eat() {
        console.log('eat food')
    }
}
複製代碼

上面這個Animal類實際等同於下面這樣:函數

function Animal() {}
Object.defineProperty(Animal.prototype, 'eat', {
    value: function() { console.log('eat food'); },
    enumerable: false,
    configurable: true,
    writable: true
});

複製代碼

類裝飾器

類裝飾器應用於類的構造函數,可用於觀察、修改或替換類定義。學習

function setDefaultDesc(constructor: Function){
    constructor.prototype.desc = '類裝飾器屬性'
}

@setDefaultDesc
class Animal {
  name: string;
  desc: string;
  constructor() {
    this.name = 'dog';
  }
}

let animal= new Animal();

console.log(animal.desc) // '類裝飾器屬性'
複製代碼

下面是使用重載函數的例子。

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        desc = "override";
    }
}

@classDecorator
class Animal {
    property = "property";
    desc: string;
    constructor(m: string) {
        this.desc = m;
    }
}

console.log(new Animal("world")); // Animal: {property: "property", desc: "override", newProperty: "new property" }
複製代碼

這部分代碼的含義是:被classDecorator裝飾的類裏面若是不存在newPropertydesc屬性,會增長相應的屬性和對應的value,若是存在該屬性就會重寫該屬性的value

方法裝飾器

方法裝飾器聲明在一個方法的聲明以前(緊靠着方法聲明)。 它會被應用到方法的 屬性描述符上,能夠用來監視,修改或者替換方法定義。

方法裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:

  1. target 當前對象的原型 (若是Employee是對象,即Employee.prototype
  2. propertyKey 方法的名稱
  3. descriptor 方法的屬性描述符Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
function logMethod(
    target: Object,
    propertyName: string,
    propertyDesciptor: PropertyDescriptor): PropertyDescriptor {
    
    // target === Employee.prototype
    // propertyName === "greet"
    // propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")
    
    const method = propertyDesciptor.value;

    propertyDesciptor.value = function (...args: any[]) {

        // 將參數列表轉換爲字符串
        const params = args.map(a => JSON.stringify(a)).join();

        // 調用該方法並讓它返回結果
        const result = method.apply(this, args);

        // 轉換結果爲字符串
        const r = JSON.stringify(result);

        // 在控制檯中顯示函數調用細節
        console.log(`Call: ${propertyName}(${params}) => ${r}`);

        // 返回調用的結果
        return result;
    }
    return propertyDesciptor;
};

class Employee {

    constructor(
        private firstName: string,
        private lastName: string
    ) {
    }

    @logMethod
    greet(message: string): string {
        return `${this.firstName} ${this.lastName} : ${message}`;
    }

}

const emp = new Employee('三月風情', '陌上花開');
emp.greet('三月風情陌上花'); // return: '三月風情 陌上花開 : 三月風情陌上花'

複製代碼

訪問器裝飾器

訪問器只是類聲明中屬性的getter和setter部分。 訪問器裝飾器是在訪問器聲明以前聲明的。訪問器裝飾器應用於訪問器的屬性描述符,可用於觀察、修改或替換訪問器的定義。

訪問器裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:

  1. target 當前對象的原型 (若是Employee是對象,即Employee.prototype
  2. propertyKey 方法的名稱
  3. descriptor 方法的屬性描述符Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)

下面是使用了訪問器裝飾器(@configurable)的例子,應用於Point類的成員上:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}
複製代碼

屬性裝飾器

屬性裝飾器函數須要兩個參數:

  1. target 當前對象的原型
  2. propertyKey 屬性的名稱
function logParameter(target: Object, propertyName: string) {

    // 屬性的值
    let _val = target[propertyName];

    // 屬性的get方法
    const getter = () => {
        console.log(`Get: ${propertyName} => ${_val}`);
        return _val;
    };

    // 屬性的set方法
    const setter = newVal => {
        console.log(`Set: ${propertyName} => ${newVal}`);
        _val = newVal;
    };

    // 刪除屬性.
    if (delete target[propertyName]) {

        // 使用getter和setter建立新屬性
        Object.defineProperty(target, propertyName, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true
        });
    }
}

class Employee {

    @logParameter
    name: string;

}

const emp = new Employee();

emp.name = '陌上花開'; // Set: name => 陌上花開

console.log(emp.name);
// Get: name => 陌上花開
// 陌上花開

複製代碼

參數裝飾器

參數裝飾函數須要三個參數:

  1. target 當前對象的原型
  2. propertyKey 方法的名稱
  3. index 參數在參數數組中的位置
function logParameter(target: Object, propertyName: string, index: number) {

    // 爲相應的方法生成元數據
    // 保持被修飾參數的位置
    const metadataKey = `log_${propertyName}_parameters`;
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    }
    else {
        target[metadataKey] = [index];
    }
}

class Employee {
    greet(@logParameter message: string): string {
        return `hello ${message}`;
    }
}
const emp = new Employee();
emp.greet('hello');

複製代碼

在上面的代碼示例中:target爲Employee的實例emp,propertyName的值爲greet,index的值爲0

裝飾器工廠

咱們先假設這樣一個場景,好比咱們須要幾個裝飾器,分別把一個類中的部分屬性、類自己、方法、參數的名稱打印出來,這時候咱們該怎麼作。

import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';

// 假設咱們已經有了上面這些裝飾器,下面咱們就該這樣作。

function log(...args) {
    switch (args.length) {
        case 3: 
            // 能夠是方法裝飾器仍是參數裝飾器
            if (typeof args[2] === "number") { 
                // 若是第三個參數是number那麼它的索引就是它的參數裝飾器
                return logParameter.apply(this, args);
            }
            return logMethod.apply(this, args);
        case 2: 
            // 屬性裝飾器
            return logProperty.apply(this, args);
        case 1: 
            // 類裝飾器
            return logClass.apply(this, args);
        default:
            // length長度在1,2,3外的狀況 
            throw new Error('Not a valid decorator');
    }
}

@log
class Employee {

    @log
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    @log
    greet(@log message: string): string {
        return `${this.name} says: ${message}`;
    }

}
複製代碼

裝飾器工廠就是一個簡單的函數,它返回一種類型的裝飾器。

元數據Reflection API

@Reflect.metadata('name', 'A')
class A {
  @Reflect.metadata('hello', 'world')
  public hello(): string {
    return 'hello world'
  }
}

Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
複製代碼
  • Relfect Metadata,能夠經過裝飾器來給類添加一些自定義的信息
  • 而後經過反射將這些信息提取出來
  • 也能夠經過反射來添加這些信息

反射, ES6+ 加入的 Relfect 就是用於反射操做的,它容許運行中的 程序對自身進行檢查,或者說「自審」,並能直接操做程序的內部屬性和方法,反射這個概念其實在 Java/c# 等衆多語言中已經普遍運用了

再來一個小例子來看下:

function logParameter(target: Object, propertyName: string, index: number) {
    // 從目標對象獲取元數據
    const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
    indices.push(index);
    // 將元數據定義爲目標對象
    Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}

// 屬性裝飾器使用反射api來獲取屬性的運行時類型
export function logProperty(target: Object, propertyName: string): void {
    // 從對象中獲取屬性的設計類型
    var t = Reflect.getMetadata("design:type", target, propertyName);
    console.log(`${propertyName} type: ${t.name}`); // name type: String
}


class Employee {
  
    @logProperty
    private name: string;
    
    constructor(name: string) {
        this.name = name;
    }

    greet(@logParameter message: string): string {
        return `${this.name} says: ${message}`;
    }

}
複製代碼

在上面的例子中,咱們使用了反射元數據設計鍵[design:type]。目前只有三種:

  • 類型元數據使用元數據鍵design:type
  • 參數類型元數據使用元數據鍵design:paramtypes
  • 返回類型的元數據使用元數據鍵design:returntype

總結

這篇文章主要介紹了類裝飾器,方法裝飾器,訪問器裝飾器,屬性裝飾器,參數裝飾器,裝飾器工廠,和元數據Reflection。也是我學習過程當中的一些總結。每週都會持續更新不一樣的技術,喜歡的同窗能夠點贊加關注,你們一塊兒進步。若是有想學習某方面技術的同窗也歡迎評論區留言,我會努力寫出你們感興趣的內容。

相關文章
相關標籤/搜索