TypeScript装饰器,一篇就够了

本文介绍TypeScript装饰器的原理

预备知识

属性描述符概念

TypeScript装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

类装饰器

接口定义

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

类装饰器表达式,由定义知道,传入1个参数:

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

先看一个最简单的装饰器,普通装饰器,只有一个参数target,当把这个@helloWord装饰器作用在HelloWordClass类上,这个target参数传递的就是HelloWordClass类。

// decorator.ts 创建这个文件
function helloWord(target: any) {
    console.log('hello Word!');
}

@helloWord
class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    name: string = 'zzb';
}

执行编译

tsc decorator.ts

这时会解析成decorator.js文件

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function helloWord(target) {
    console.log('hello Word!');
}
var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        this.name = 'zzb';
        console.log('我是构造函数');
    }
    HelloWordClass = __decorate([
        helloWord
    ], HelloWordClass);
    return HelloWordClass;
}());

装饰器是编译时就被翻译成可读性的代码,现在把上面分成三个部分来解析。

@helloWord类装饰器解析

	var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

第1行定义了__decorate函数,该函数就是@helloWord解析出来的,用来处理类装饰器的功能。

(this && this._decorate)

首先this指向window,判断当前window实例中是否已经存在_decorate变量,window这个全局变量中并没有_decorate变量,所以该表达式的结果为false。得到:

var __decorate = function (decorators, target, key, desc) {...}

这里有4个参数,decorators接收数组,包含多个装饰器函数。target表示被装饰的类,也就是HelloWordClass()构造函数。key和desc没有使用,所以为undefined,但在其他的类型的装饰器会使用。

第2行, arguments.length为2,所以变量r = target,指向类的构造函数。

var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;

关于Object.getOwnPropertyDescriptor,获取对象中属性的描述符,建议查看MDN文档
属性的定义: 由一个字符串类型的“名字”(name)和一个“属性描述符”(property descriptor)对象构成。

简化得到:

var c = 2, r =  target, d;

第3、4行,先判断是否支持反射Reflect,默认ES6是没有提供Reflect.decorate方法,还不清楚这里是通过哪里调用的。所以if语句中的结果为flase,走else中的方法。
再推荐一个反射强大的库reflect-metadata

	if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);

    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;

从for循环语句可以知道,decorators这个数组是先处理后面的g函数,相当于从后往前执行。等价于f(g(method))。

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

在我们例子中,就只有一个函数,前面得知c=2,r=target,所以r = d®,相当于就是执行把target作为参数调用装饰器函数的结果赋值给r,简化得:

r = d(r) || r;
// 等价于r = helloWord(target); 这里就是调用装饰器函数的时机,当d(r)没有返回值时,该d(r)表达式为undefined

最后一行

return c > 3 && r && Object.defineProperty(target, key, r), r;

return中使用逗号表达式, 相当于就是执行了语句c > 3 && r && Object.defineProperty(target, key, r),再返回逗号右侧的值。

c > 3 && r && Object.defineProperty(target, key, r)return r;

最终得到简化结果:

var __decorate = function (decorators, target, key, desc) {
	var c = 2, r = target, d;
	for (var i = decorators.length - 1; i >= 0; i--)
		if (d = decorators[i]) r = d(r) || r; // 如果d(r)没有返回值,则d(r) || r 等价于 undefined || target,返回原来的类
	return r;
}
function helloWord(target) {
    console.log('hello Word!');
}
var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        this.name = 'zzb';
        console.log('我是构造函数');
    }
    HelloWordClass = __decorate([
        helloWord
    ], HelloWordClass);
    return HelloWordClass;
}());

函数自执行

从上面的简化结果看,装饰器函数helloWord()并没有被修改,但是类HelloWordClass被解析成一个自执行函数。

var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        this.name = 'zzb';
        console.log('我是构造函数');
    }
    HelloWordClass = __decorate([
        helloWord
    ], HelloWordClass);
    return HelloWordClass;
}());

在自执行函数中,HelloWordClass接收__decorate()执行的结果,相当于就是改变了构造函数,所以可以利用装饰器修改类的功能。

由于是自执行函数,在程序运行起来,装饰器函数helloWord()就会被执行一次,所以会看到控制台输出。

'hello Word!'

就算之后通过new HelloWordClass();也不会再输出’hello Word!’。

类装饰器3种类型

在这篇文章就有讲到这三种用法

  • 普通装饰器(无法传参)
  • 装饰器工厂(可传参)
  • 重载构造函数

普通装饰器(无法传参)

function helloWord(target: any) {
    console.log('hello Word!');
}

@helloWord
class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    name: string = 'zzb';
}

装饰器工厂(可传参)

增加了一个静态变量

function helloWord(isTest: boolean) {
    return function(target: any) {
        // 添加静态变量
        target.isTestable = isTest;
    }
}

@helloWord(false)
class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    name: string = 'zzb';
}
let p = new HelloWordClass();
console.log(HelloWordClass.isTestable);

重载构造函数

function helloWord(target: any) {
    return class extends target {
        sayHello(){
            console.log("Hello")
        }
    }
}

@helloWord
class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    name: string = 'zzb';
}

属性装饰器

属性装饰器接口定义

declare type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void;

属性装饰器表达式会在运行时当作函数被调用,由定义知道,传入2个参数:

  1. target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. propertyKey —— 属性的名称。
    没有返回值。

按照上面的接口形式,定义了一个defaultValue()装饰器方法,就算是用private也是能生效的。

function defaultValue(value: string) {
    return function (target: any, propertyName: string) {
        target[propertyName] = value;
    }
}

class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    @defaultValue('zzb')
    private name: string | undefined;
}
let p = new HelloWordClass();
console.log(p.name);

输出结果:

我是构造函数
zzb // 这里打印出设置的默认值

转换后,在自执行函数中,__decorate()传入了三个参数,装饰器、构造函数的原型和属性名,简化得到最终结果:

// 简化结果
var __decorate = function (decorators, target, key, desc) {
	var c = 3, r = undefined, d;
	for (var i = decorators.length - 1; i >= 0; i--)
		if (d = decorators[i]) r = d(target, key) || r; // d(target, key)没有返回值,则r= undefined || undefined
	return r;
}
function defaultValue(value) {
    return function (target, propertyName) {
        target[propertyName] = value;
        //解析成 HelloWordClass.prototype["name"] = 'zzb'
    };
}
var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        console.log('我是构造函数');
    }
    __decorate([
        defaultValue('zzb')
    ], HelloWordClass.prototype, "name");
    return HelloWordClass; // 这里返回原本的类
}());

以下改为,静态成员变量。

function defaultValue(value: string) {
    return function (target: any, propertyName: string) {
        target[propertyName] = value;
    }
}

class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    @defaultValue('zzb')
    static nameVar: string | undefined; //因为类的静态变量name,会输出类的名字,为了避免干扰,这个改了一下属性名。
}
let p = new HelloWordClass();
console.log(HelloWordClass.nameVar);

转换后:

var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        console.log('我是构造函数');
    }
    __decorate([
        defaultValue('zzb')
    ], HelloWordClass, "nameVar"); // 第二个参数不同
    return HelloWordClass;
}());

调用__decorate的第二个参数不同,是类的构造函数,其余都相同。

通过属性描述来修改属性值

对于属性的装饰器,是没有返回descriptor的,并且装饰器函数的返回值也会被忽略掉。还可以通过自己获取descriptor,并进行修改。

function defaultValue(value: string) {
    return function (target: any, propertyName: string) {
        let descriptor = Object.getOwnPropertyDescriptor(target, propertyName);

        Object.defineProperty(target, propertyName, {
            ...descriptor,
            value
        })
    }
}

class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    @defaultValue('zzb')
    private name: string | undefined;
}
let p = new HelloWordClass();
console.log(p.name);

这种方式通过Object.getOwnPropertyDescriptor和Object.defineProperty方法对属性进行定义。这种方式适用面更广,可以针对属性描述进行修改。

方法装饰器

给类中的方法添加装饰器。如果不理解属性描述符,先

declare type MethodDecorator = <T>(
  target: Object, propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>) =>
    TypedPropertyDescriptor<T> | void;

方法装饰器接受三个参数:

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

返回属性描述符或者没有返回。

例子:给原来的方法增加了调用时间的统计。

function logFunc(params: string) {
    return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
        // target === HelloWordClass.prototype
        // propertyName === "sayHello"
        // propertyDesciptor === Object.getOwnPropertyDescriptor(HelloWordClass.prototype, "sayHello")

        console.log(params);
        // 被装饰的函数
        const method = descriptor.value;
        descriptor.value = function (...args: any[]) {
            let start = new Date().valueOf();
            // 将 sayHello 的参数列表转换为字符串
            args = args.map(arg => String(arg));
            console.log('参数args = ' + args);
            try {
                // // 调用 sayHello() 并获取其返回值
                return method.apply(this, args)
            } finally {
                let end = new Date().valueOf();
                console.log(`start: ${start} end: ${end} consume: ${end - start}`)
            }
        };
        return descriptor;
    }
}

class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    private nameVar: string | undefined;

    @logFunc('log装饰器')
    sayHello(name: string) {
        console.log(name + ' sayHello');
    }
}
let pHello = new HelloWordClass();
pHello.sayHello('zzb');

控制台输出结果:

log装饰器
我是构造函数
参数args = zzb
zzb sayHello
start: 1574331292433 end: 1574331292434 consume: 1

转换后:

function logFunc(params) {
    return function (target, propertyName, descriptor) {
        // target === HelloWordClass.prototype
        // propertyName === "sayHello"
        // propertyDesciptor === Object.getOwnPropertyDescriptor(HelloWordClass.prototype, "sayHello")
        console.log(params);
        // 被装饰的函数
        var method = descriptor.value;
        descriptor.value = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            var start = new Date().valueOf();
            args = args.map(function (arg) { return String(arg); });
            console.log('参数args = ' + args);
            try {
                return method.apply(this, args);
            }
            finally {
                var end = new Date().valueOf();
                console.log("start: " + start + " end: " + end + " consume: " + (end - start));
            }
        };
        return descriptor;
    };
}
var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        console.log('我是构造函数');
    }
    HelloWordClass.prototype.sayHello = function (name) {
        console.log(name + ' sayHello');
    };
    __decorate([
        logFunc('log装饰器')
    ], HelloWordClass.prototype, "sayHello", null);
    return HelloWordClass;
}());
var pHello = new HelloWordClass();
pHello.sayHello('zzb');

分析:

__decorate([
        logFunc('log装饰器')
    ], HelloWordClass.prototype, "sayHello", null);

从这一段代码可以知道,__decorate()传递了4个参数,关键是传入了第4个参数为null,简化后:

var __decorate = function (decorators, target, key, desc) {
	var c = 4, r = desc = Object.getOwnPropertyDescriptor(target, key), d;
	for (var i = decorators.length - 1; i >= 0; i--)
		if (d = decorators[i]) r = d(target, key, r) || r;// d(target, key, r)如果没有返回值,则为r = undefined || r
	return c > 3 && r && Object.defineProperty(target, key, r), r;
}

由于装饰器在编译时就被执行,所以控制的输出结果就比较好理解了。

log装饰器 //装饰器执行
我是构造函数 // 在new HelloWordClass();执行
参数args = zzb // 以下三个 都是在sayHello()中输出
zzb sayHello
start: 1574331292433 end: 1574331292434 consume: 1

方法参数装饰器

方法参数装饰器接口定义

declare type ParameterDecorator = 
(target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

方法参数装饰器会接收三个参数:

  1. target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. propertyKey —— 属性的名称。
  3. parameterIndex —— 参数数组中的位置。
function logParameter(target: any, propertyName: string, index: number) {
    // 为相应方法生成元数据键,以储存被装饰的参数的位置
    const metadataKey = `log_${propertyName}_parameters`;
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    } else {
        target[metadataKey] = [index];
    }
}


class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }
    private nameVar: string | undefined;

    sayHello(@logParameter name: string) {
        console.log(name + ' sayHello');
    }
}
let pHello = new HelloWordClass();
pHello.sayHello('zzb');

转换和简化得:

// 装饰器方法
function logParameter(target, propertyName, index) {
    // 为相应方法生成元数据键,以储存被装饰的参数的位置
    var metadataKey = "log_" + propertyName + "_parameters";
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    }
    else {
        target[metadataKey] = [index];
    }
}
var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        console.log('我是构造函数');
    }
    HelloWordClass.prototype.sayHello = function (name) {
        console.log(name + ' sayHello');
    };
    __decorate([
        __param(0, logParameter) 
    ], HelloWordClass.prototype, "sayHello", null);
    return HelloWordClass;
}());

其中

__decorate([
        __param(0, logParameter) 
    ], HelloWordClass.prototype, "sayHello", null);

从这一段代码可以知道,__decorate()传递了4个参数,关键是传入了第4个参数为null。先执行了一个__param(0, logParameter) 中间函数,用来获取当前参数的索引位置。

var __decorate = function (decorators, target, key, desc) {
	var c = 4, r = desc = Object.getOwnPropertyDescriptor(target, key), d;
	for (var i = decorators.length - 1; i >= 0; i--)
		if (d = decorators[i]) r = d(target, key, r);// r接收装饰器方法的返回值,也就是void
	return r;
}

// 返回接受参数索引和装饰器的函数
var __param = (this && this.__param) || function (paramIndex, decorator) {
	// 这里返回了一个装饰器
    return function (target, key) { 
    	decorator(target, key, paramIndex); 
    }
};

其中d(target, key, r)就等于执行了__param(0, logParameter)中return的方法。在logParameter是没有返回值的,所以d(target, key, r)的返回值结果为undefined。

r = d(target, key, r) || r; 等价于 r = undefined || r; // 右侧的r为属性描述符

访问器装饰器

访问器就是添加有get、set前缀的函数,用于控制属性的赋值及取值操作。
访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。

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

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

例子:

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

class HelloWordClass {
    constructor() {
        console.log('我是构造函数')
    }

    private _name: string = 'zzb';
    private _age: number = 10;

    @enumerable(true)
    get name() {
        return this._name;
    }

    set name(name: string) {
        this._name = name;
    }

    @enumerable(false)
    get age() {
        return this._age;
    }

    set age(age: number) {
        this._age = name;
    }
}
let pHello = new HelloWordClass();
for (let prop in pHello) {
    console.log(`property = ${prop}`);
}

enumerable属性描述符:
当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。通过Object.defineProperty()创建属性,enumerable默认为 false。

如果一个属性的enumerable为false,通过对象还是能访问的到这个属性,但下面三个操作不会取到该属性。

  • for…in循环
  • Object.keys方法
  • JSON.stringify方法

我们定义了两个访问器 name 和 age,并通过装饰器设置是否将其列入清单,据此决定对象的行为。name 将列入清单,而 age 不会。
所以控制台输出结果:

我是构造函数 
property = _name
property = _age
property = name
// 少了一个属性age

注意:TypeScript 不允许同时装饰单一成员的 get 和 set 访问器。这是因为装饰器可以应用于属性描述符,属性描述符结合了 get 和 set 访问器,而不是分别应用于每项声明。

转换和简化后结果:

var __decorate = function (decorators, target, key, desc) {
	var c = 4, r = desc = Object.getOwnPropertyDescriptor(target, key), d;
	for (var i = decorators.length - 1; i >= 0; i--)
		if (d = decorators[i]) r = d(target, key, r) || r;// d(target, key, r)计算的结果为undefined,如果r为属性描述符
	return r;
}
function enumerable(value) {
    return function (target, propertyKey, descriptor) {
        descriptor.enumerable = value;
    };
}
var HelloWordClass = /** @class */ (function () {
    function HelloWordClass() {
        this._name = 'zzb';
        this._age = 10;
        console.log('我是构造函数');
    }
    Object.defineProperty(HelloWordClass.prototype, "name", {
        get: function () {
            return this._name;
        },
        set: function (name) {
            this._name = name;
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(HelloWordClass.prototype, "age", {
        get: function () {
            return this._age;
        },
        set: function (age) {
            this._age = name;
        },
        enumerable: true,
        configurable: true
    });
    __decorate([
        enumerable(true)
    ], HelloWordClass.prototype, "name", null);
    __decorate([
        enumerable(false)
    ], HelloWordClass.prototype, "age", null);
    return HelloWordClass;
}());
var pHello = new HelloWordClass();
for (var prop in pHello) {
    console.log("property = " + prop);
}

这个还是比较好理解,就是将age的属性给隐藏起来,无法被上面三个查询操作发现。

装饰器执行顺序

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

装饰器函数总结

类装饰器类型定义

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

属性装饰器类型定义

declare type PropertyDecorator =
  (target: Object, propertyKey: string | symbol) => void;

方法装饰器和访问符装饰器类型定义

declare type MethodDecorator = <T>(
  target: Object, propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>) =>
    TypedPropertyDescriptor<T> | void;

方法参数装饰器类型定义

declare type ParameterDecorator = 
(target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

不同情况__decorate解析汇总

前面每个分类都讲解了__decorate这个函数,这里作为总结。先简化一下,去掉Reflect相关操作。

var __decorate = function (decorators, target, key, desc) {

	// 当c = 2个参数,类装饰器, r = target,被装饰的类
	// 当c = 3个参数,属性装饰器, r = undefined
	// 当c = 4个参数,方法、访问符、方法参数装饰器, r = Object.getOwnPropertyDescriptor(target, key) 获取属性描述符
	var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;	
	for (var i = decorators.length - 1; i >= 0; i--) 
		if (d = decorators[i]) 
			r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;      
	return c > 3 && r && Object.defineProperty(target, key, r), r;
}

对以下代码提取出来分析

r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 
  • 当c = 2个参数,类装饰器。根据类装饰器接口定义,返回类型为TFunction | void。r = d( r ) || r; d( r )可能会返回新的构造函数, 也可能没有返回值,没有返回值的情况,等价于undefined || target,返回原来的类。
  • 当c = 3个参数,属性装饰器。r = d(target, key) || r; 根据属性装饰器接口定义,返回类型为void。d(target, key)没有返回值,则r= undefined || undefined。
  • 当c = 4个参数,方法、访问符、方法参数装饰器。
    根据方法装饰器接口定义,返回类型为TypedPropertyDescriptor | void。
    d(target, key, r)可能是一个新的属性描述符,也可能为void。 而方法参数装饰器返回类型只有void。不管怎样 r = d(target, key, r) || r; 接收到的都是属性描述符,所以只有方法、访问符、方法参数装饰器这三种情况会执行下面的操作,给类定义一个方法,并按照r这样的属性描述符创建值。
c > 3 && r && Object.defineProperty(target, key, r);

总结

继续学习typeScript在实际中的应用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值