本文介绍TypeScript装饰器的原理
属性描述符概念
装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
接口定义
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
类装饰器表达式,由定义知道,传入1个参数:
先看一个最简单的装饰器,普通装饰器,只有一个参数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;
}());
装饰器是编译时就被翻译成可读性的代码,现在把上面分成三个部分来解析。
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!’。
在这篇文章就有讲到这三种用法
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个参数:
按照上面的接口形式,定义了一个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;
方法装饰器接受三个参数:
返回属性描述符或者没有返回。
例子:给原来的方法增加了调用时间的统计。
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;
方法参数装饰器会接收三个参数:
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个参数:
例子:
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,通过对象还是能访问的到这个属性,但下面三个操作不会取到该属性。
我们定义了两个访问器 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的属性给隐藏起来,无法被上面三个查询操作发现。
类中不同声明上的装饰器将按以下规定的顺序应用:
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这个函数,这里作为总结。先简化一下,去掉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 > 3 && r && Object.defineProperty(target, key, r);
继续学习typeScript在实际中的应用。