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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容