Typescript —— 装饰器

概念介绍

装饰器模式(Decorator Pattern)允许像一个现有的对象添加新的功能,同时又不改变其结构,这种类型的设计模式属于结构型模式,他是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名的完整性的前提下,提供了额外的功能。

我们通过下面的实例来演示装饰器模式的用法。其中,我们把一个形状装饰上不同的颜色,同时又不改变形状类

用js实现一个装饰器,看下原理
const dec = (target, property) => {
    const old = target.prototype[property];
    target.prototype[property] = (msg) => {
        msg = `【${msg}】`;
        old(msg)
    }

}
class Log {
    print(str: string) {
        console.log(str);
    }
}
dec(Log, "print");
const log = new Log()
log.print("你好")

在控制台里会打印出如下结果:

【你好】

关于类、方法、访问器、属性装饰器(Typescript中)

  • 类装饰器:接收1个参数(target)、可以用来监视、修改和替换类定义
  • 方法装饰器:接收3个参数(target、property、descriptor)可以用来监视、修改和替换方法定义
  • 访问器装饰器:接收3个参数(target、property、descriptor)可以用来监视、修改和替换访问器定义
  • 属性装饰器:接收2个参数(target、property)只能用来监视类中是否声明了某个名字的属性
  • 参数装饰器:接收3个参数(target、property、paramsIndex)只能用来监视一个方法的参数是否被传入。

详解:
target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象(prototype)
property:成员的名字
descriptor:成员的属性描述符
paramsIndex:参数的下标

descriptor简单介绍

//descriptor 属性描述符
//在方法装饰器中
{
  value: [Function],
  writable: true,
  enumerable: true,
  configurable: true
}
//在访问器装饰器中
{
  get: [Function: get],
  set: undefined,
  enumerable: false,
  configurable: true
}

类装饰器

类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

注意 类的构造函数作为其唯一的参数,如果类装饰器返回一个值,他会使用提供的构造函数来替换类的声明

const anotation: Function = (target) => {
    console.log(target.foo);
    console.log(target.method);
    return class extends target {
        newMethod() {
            console.log("Example New Method");
        }
    }
}

@anotation
class Example {
    static foo = "fooValue";
    public method() {
        console.log("Example Public Method");

    }
}
const example = new Example();
example["newMethod"]();

在控制台里会打印出如下结果:

fooValue
undefined
Example New Method

方法装饰器

用来监视,修改或者替换方法定义。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数(target,property,descriptor):

  1. target:对于静态成员来说是类的构造函数(包含全部的静态属性),对于实例成员是类的原型对象(包含全部的实例属性)。
  2. property:成员的名字。
  3. descriptor:成员的属性描述符。

第一种return target装饰函数(想使用target装饰函数,必须return)

// 方法装饰器(return target)
const anotationMethod: Function = (target, property, descriptor) => {
    const old = target[property];
    target[property] = function () {
        console.log("old calling");
        old();
    }
    return target
}
class Example {
    static foo = "fooValue";

    @anotationMethod
    public method() {
        console.log("Example Public Method");

    }
}
const example = new Example();
example.method();

在控制台里会打印出如下结果:

old calling
Example Public Method

2.第二种descriptor.value装饰函数(直接修改descriptor.value即可)

// 方法装饰器(修改descriptor.vaue)
const anotationMethod: Function = (target, property, descriptor) => {
    const old = descriptor.value;
    descriptor.value = function () {
        console.log("old calling");
        old();
    }
}
// 方法装饰器(修改descriptor.value)
class Example {
    static foo = "fooValue";
    @anotationMethod
    public method() {
        console.log("Example Public Method");

    }
}
const example = new Example();
example.method();

在控制台会打印出如下结果

old calling
Example Public Method

访问器装饰器

访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。

注意  TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

注意  如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。


const configurable: Function = (target, property, descriptor: PropertyDescriptor) => {
    console.log(target);
    console.log(property);
    console.log(descriptor);
    descriptor.get = () => {
        return "杨志强"
    }
}
class Point {
    private _name: string = "张三";

    @configurable
    get name() {
        return this._name
    }
    set name(newVal) {
        this._name = newVal;
    }
}

const point = new Point();
console.log(point.name);

在控制台会输出如下结果

Point {}
name
{
  get: [Function: get],
  set: [Function: set],
  enumerable: false,
  configurable: true
}
杨志强

属性装饰器

属性装饰器只能用来监视类中是否声明了某个名字的属性。
因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。

属性装饰器表达式会在运行时当作函数被调用,传入下列俩个参数

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

注意 属性描述符不会作为参数传入属性装饰器,这与Typescript是如何初始化属性装饰器有关

import "reflect-metadata"
const formatMetadataKey = Symbol("format");
const format = (value: string) => {
    return Reflect.metadata(formatMetadataKey, value);
}
function getFormat(target, property) {
    return Reflect.getMetadata(formatMetadataKey, target, property);
}

class Point {
    @format("Hello World")
    public name: string = "张三";

    greet(property) {
        return getFormat(this, property);
    }
}

const point = new Point();
console.log(point.greet("name"));

这个@format("Hello World")装饰器是个 装饰器工厂。 当 @format("Hello World")被调用时,它会添加一条这个属性的元数据,通过reflect-metadata库里的Reflect.metadata函数。 当 getFormat被调用时,它读取格式的元数据。

在控制台会打印如下结果

Hello World

参数装饰器

参数装饰器只能用来监视一个方法的参数是否被传入。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

import "reflect-metadata";
const requiredKey = Symbol("requeiredKey")
const required: Function = (target, property, paramIndex) => {
    let paramIndexs = Reflect.getMetadata(requiredKey, target, property) || [];
    paramIndexs.push(paramIndex);
    Reflect.defineMetadata(requiredKey, paramIndexs, target, property);
}

// 此处为什么能用getOwnMetadata也能接收到?
// 答:因为装饰器接收的target对于实例成员来说类的原型对象(prototype),对于静态成员来说是类的构造函数,所以我们required和validate指向同一个对象(target.prototype),所以能获取元数据
const validate: Function = (target, property, descriptor: PropertyDescriptor) => {
    const method = descriptor.value;
    descriptor.value = function () {
        const requeiredParamers = Reflect.getOwnMetadata(requiredKey, target, property);
        if (requeiredParamers) {
            for (let paramIndex of requeiredParamers) {
                if (paramIndex >= arguments.length || arguments[paramIndex] === undefined) {
                    throw new Error("参数不正确");
                }
            }
        }
        method.apply(this, arguments);
    }

}

class User {
    name: string = "张三"
    @validate
    info(@required id?: number) {
        console.log(`用户id为${id}`);

    }

}
const user = new User();
user.info(1);

在控制台里面打印会打印如下结果

用户id为1

关于元数据方法的补充

Reflect.getMetadata(metadataKey,target,property):获取目标对象或其原型链上提供的元数据键的元数据值。
Reflect.getOwnMetadata(metadataKey,target,property)::获取目标对象上提供的元数据键的元数据值(目标对象指的不是this,而是未实例化的这个类)。
Reflect.defineMetadata(metadataKey,value,target,property):在目标上定义唯一的元数据项。
Reflect.metadata(metadataKey,value):可用于类、类成员或参数的默认元数据装饰器工厂。(一般直接return)

关于装饰器的执行顺序

如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:

function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

function g() {
    console.log("g(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): called");
    }
}

class C {
    @f()
    @g()
    method() {}
}

在控制台里面打印会打印如下结果

f(): evaluated
g(): evaluated
g(): called
f(): called

推荐阅读更多精彩内容