前端深入理解TypeScript装饰器(Decorators)概念

1,957 阅读6分钟

分享一下学习装饰器(Decorators)的所得

概念介绍

装饰器是TypeScript提供的最强大的功能之一,它使我们能够以干净的声明性方式扩展类和方法的功能。装饰器目前是JavaScript 的第2阶段提议,但在TypeScript生态系统中已受到欢迎,主要的开放源代码项目(例如Angular)正在使用装饰器。

本人工作中是使用Angular8进行项目一个项目的开发的,接触装饰器这一方面也就比较多,所以就趁着周末整理了一篇关于装饰器Decorators的文章,也希望能帮助到学习这方面的同学们。废话不多说,下面咱们进入正题。

开始使用装饰器Decorators

首先我们要在tsconfig.json里面启用experimentalDecorators编译器选项:

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

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

我们先明确两个概念:

  • 目前装饰器本质上是一个函数,@expression的形式其实是一个语法糖,expression求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
  • JavaScript中的Class其实也是一个语法糖。

例如我们在Javascript中声明一个Class:

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。也是我学习过程中的一些总结。每周都会持续更新不同的技术,喜欢的同学可以点赞加关注,大家一起进步。如果有想学习某方面技术的同学也欢迎评论区留言,我会努力写出大家感兴趣的内容。