装饰器 是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
预备知识
装饰器工厂
如果我们要定制一个修饰器应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
例如:
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
属性描述符
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
/**
* obj: 要定义属性的对象。
* prop: 要定义或修改的属性的名称或 Symbol 。
* descriptor: 要定义或修改的属性描述符。
*/
Object.defineProperty(obj, prop, descriptor)
该方法允许精确地添加或修改对象的属性。默认的情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。
数据描述符
数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。
存取描述符
存取描述符是由 getter 函数和setter 函数所描述的属性。
Reflect
概述
Reflect是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。
让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name) 和Reflect.deleteProperty(obj, name) 让它们变成了函数行为。
Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在Reflect 上获取默认行为。
类装饰器
类装饰器应用于类构造函数,可以用来监视,修改或替换类定义
接口定义:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
首先,写一个最简单的装饰器:
decorator.ts :
function helloWorld(target: any) {
console.log('hello World!')
console.log('target :', target.toString())
}
@helloWorld
class HelloWorldClass {
name: string = 'jerome'
constructor() {
console.log('I am constructor.')
}
test() {
console.log('I am test method.')
}
}
运行 tsc -p . 编译 ts 文件,生成 js 文件,并用 node 执行这个 js 文件:
$ node decorator/decorator.js
hello World!
target : function HelloWorldClass() {
this.name = 'jerome';
console.log('I am constructor.');
}
由此可见,装饰器在运行时就被执行,target 传递的就是 HelloWorldClass 类的构造函数,也印证了装饰器的定义中,它在运行时被调用,被装饰的声明信息作为参数传入。
接下来,我们解析一下这个 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 helloWorld(target) {
console.log('hello World!');
console.log('target :', target.toString());
}
var HelloWorldClass = /** @class */ (function () {
function HelloWorldClass() {
this.name = 'jerome';
console.log('I am constructor.');
}
HelloWorldClass.prototype.test = function () {
console.log('I am test method.');
};
HelloWorldClass = __decorate([
helloWorld
], HelloWorldClass);
return HelloWorldClass;
}());
代码太多,让我们分几步来解析:
@helloWorld 类装饰器解析
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--)
// 如果装饰器合法,将其赋值给 d
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;
};
第一行定义了 __decorate 函数,就是通过 @helloWorld 解析出来的,用来处理装饰器的功能。
这个函数有四个参数,让我们来看看都是些什么
/**
* decorators: 数组,包含多个装饰器
* target: 被装饰的类,即 HelloWorldClass 的构造函数
* key: 变量名称
* desc: 属性描述符
*/
function (decorators, target, key, desc) {...}
c < 3 ? 为什么是 3,c 代表的是传入参数的个数,如果未传入属性描述符,r 都为 target,若传入属性描述符且不为空,则 r 为属性描述符。
通过对这段 js 的分析之后,可以简化为如下:
var c = 2, r = target, d;
for (var i = decorators.length - 1; i >= 0; i--)
if (d = decorators[i]) r = d(r) || r;
return r;
当装饰器合法时,将其赋值给 d,r = d® || r 相当于把 target 作为参数调用装饰器函数的结果赋值给 r, 如果 d® 没有返回值,返回的是原来的类 target,当有返回值的时候,返回的是 d® 的 return 内容。
HelloWorldClass 类
从打包出的结果来看,类 HelloWorld 被解析成了一个自执行函数:
var HelloWorldClass = /** @class */ (function () {
function HelloWorldClass() {
this.name = 'jerome';
console.log('I am constructor.');
}
HelloWorldClass.prototype.test = function () {
console.log('I am test method.');
};
HelloWorldClass = __decorate([
helloWorld
], HelloWorldClass);
return HelloWorldClass;
}());
在自执行函数中,HelloWorldClass 接收 __decorate() 函数的执行结果,相当于改变了构造函数,所以可以利用装饰器修改类的功能。
带返回值的类装饰器
上面的例子是没有返回值的装饰器函数,它返回的是原来的类,那么带返回值的装饰器函数是怎么样的呢?是否是被装饰器修改之后的类呢?让我们来一起探究一下:
按照之前的步骤,这次我们的装饰器要带有返回值,下面是一个 override 构造函数的例子:
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
运行的结果:
$ node decorator/decoratorReturn.js
class_1 {
property: 'property',
hello: 'override',
newProperty: 'new property'
}
由代码可知,Greeter 的构造函数是对 hello 的赋值为 world,然而在使用了 @classDecorator 这个构造函数之后,hello 的值变为了 override, 说明了类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
属性装饰器
属性装饰器声明在一个属性声明之前,属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
成员的名字。
declare type PropertyDecorator =
(target: Object, propertyKey: string | symbol) => void;
那么,如何理解呢,让我们来看段代码:
function logParameter(target: Object, propertyName: string) {
// 属性值
let _val = this[propertyName];
// 属性读取访问器
const getter = () => {
console.log(`Get: ${propertyName} => ${_val}`);
return _val;
};
// 属性写入访问器
const setter = newVal => {
console.log(`Set: ${propertyName} => ${newVal}`);
_val = newVal;
};
// 删除属性
if (delete this[propertyName]) {
// 创建新属性及其读取访问器、写入访问器
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
class Greeter {
@logParameter
greeting: string;
}
const greeter = new Greeter();
greeter.greeting = 'Jerome';
greeter.greeting
运行这段代码:
$ node decorator/prototype.js
Set: greeting => Jerome
Get: greeting => Jerome
从结果可知,在实例中使用了 getter 和 setter 方法之后,都会打印出相应的 log,即装饰器的表达式在运行的时候被调用了。
让我们来看看编译成 js 之后的 Greeter 类
var Greeter = /** @class */ (function () {
function Greeter() {
}
__decorate([
logParameter,
__metadata("design:type", String)
], Greeter.prototype, "greeting", void 0);
return Greeter;
}());
由此可见,此次 __decorate 函数传入了 4 个参数,由在类装饰器分析的 _decorate 函数可知:
从右往左运行装饰器,先运行__metadata(“design:type”, String) 表示被装饰的参数 greeting 是 String 类型,再运行 logParameter 装饰器,改写 greeting 参数的 getter 和 setter 方法。
方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey:成员的名字。
descriptor:成员的属性描述符。
同样地,让我们来看段代码:
export function logMethod(
target: Object,
propertyName: string,
propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
const method = propertyDescriptor.value;
propertyDescriptor.value = function (...args: any[]) {
// 将 greet 的参数列表转换为字符串
const params = args.map(a => JSON.stringify(a)).join();
// 调用 greet() 并获取其返回值
const result = method.apply(this, args);
// 转换结尾为字符串
const r = JSON.stringify(result);
// 在终端显示函数调用细节
console.log(`Call: ${propertyName}(${params}) => ${r}`);
// 返回调用函数的结果
return result;
}
return propertyDescriptor;
};
class Greeter {
constructor(private name: string) { }
@logMethod
greet(message: string): string {
return `${this.name} says: ${message}`;
}
}
const greeter = new Greeter('Jerome');
greeter.greet('Hello');
运行这段代码:
$ node decorator/method.js
Call: greet("Hello") => "Jerome says: Hello"
由打印结果可知,logMethod 这个方法装饰器在运行时当作了函数被调用。该函数有利于内审方法的调用,符合面向切面编程的思想。
让我们来看看编译成 js 之后的 Greeter 类:
var Greeter = /** @class */ (function () {
function Greeter(name) {
this.name = name;
}
Greeter.prototype.greet = function (message) {
return this.name + " says: " + message;
};
__decorate([
logMethod,
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", String)
], Greeter.prototype, "greet", null);
return Greeter;
}());
由此可见,此次 __decorate 函数传入了 4 个参数,由在类装饰器分析的 _decorate 函数可知:
从右往左运行装饰器,先运行__metadata(“design:returntype”, String) 表示被装饰的方法 greet 的返回值的属性是 String 类型, __metadata(“design:paramtypes”, [String]) 表示方法参数的类型是 String,__metadata(“design:type”, Function)表示被装饰的是函数类型,再运行 logMethod 装饰器,打印出了函数调用的细节。
参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
target:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey:成员的名字。
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];
}
console.log('target => ', target)
}
class Greeter {
greet(@logParameter message: string, middle:string, @logParameter name: string): string {
return `${name} : ${message}`;
}
}
const greeter = new Greeter();
greeter.greet('hello','I am middle', 'jerome');
运行打包之后的 js:
$ node decorator/parameter.js
target => Greeter { greet: [Function], log_greet_parameters: [ 2 ] }
target => Greeter { greet: [Function], log_greet_parameters: [ 2, 0 ] }
可以看到,打印了两个 log,message 和 name 的位置分别是 0 和 2。
让我们来看看编译成 js 之后的 Greeter 类:
var Greeter = /** @class */ (function () {
function Greeter() {
}
Greeter.prototype.greet = function (message, middle, name) {
return name + " : " + message;
};
__decorate([
__param(0, logParameter), __param(2, logParameter),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, String]),
__metadata("design:returntype", String)
], Greeter.prototype, "greet", null);
return Greeter;
}());
由此可见,此次 _decorate 函数也传入 4 个参数,注意第 4 个参数为 null,这在 _decorate 函数中会使 r 的值有所不同。由前面可知,装饰器从右往左运行,依次确定被装饰对象的返回值类型,参数类型以及自身的类型为 Function,这里有个不一样的 __param() 装饰器,让我们也来了解一下:
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
}
};
可见,他调用了 logParameter 函数,并传入了一个 paramIndex 表示参数的位置。由此,参数装饰器也一目了然了。
访问器装饰器
访问器装饰器应用于访问器的属性描述符,可用于观测、修改、替换访问器的定义。
function enumerable(value: boolean) {
return function (
target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('decorator - sets the enumeration part of the accessor');
descriptor.enumerable = value;
};
}
class Person {
private _age: number = 18;
private _name: string = 'jerome';
@enumerable(false)
get age() { return this._age; }
set age(age: any) { this._age = age; }
@enumerable(true)
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
}
}
const person = new Person();
for (let prop in person) {
console.log(`enumerable property = ${prop}`);
}
运行之后:
$ node decorator/defineObject.js
decorator - sets the enumeration part of the accessor
decorator - sets the enumeration part of the accessor
enumerable property = _age
enumerable property = _name
enumerable property = name
上面的例子中,我们定义了两个访问器 name 和 age ,并通过装饰器设置是否将其列入可枚举属性,我们把 age 设置为false, 所以在清单中不会出现 age。
元数据
Reflect Metadata是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。
可以通过 npm 安装这个库:
npm i reflect-metadata --save
TypeScript 支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json 里启用 emitDecoratorMetadata 编译器选项。
命令行:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
当启用后,只要 reflect-metadata 库被引入了,设计阶段添加的类型信息可以在运行时使用。元信息反射 API 能够用来以标准方式组织元信息。「反射」的意思是代码可以侦测同一系统中的其他代码(或其自身)。反射在组合/依赖注入、运行时类型断言、测试等使用场景下很有用。
在这里,我们能够使用之前学过的各类装饰器来修饰你的代码。
例如:
import "reflect-metadata";
// 【参数装饰器】用来存储被装饰参数的索引
export function logParameter(target: Object, propertyName: string, index: number) {
const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
indices.push(index);
console.log('indices => ', indices);
Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}
// 【属性装饰器】用来获取属性的运行时类型
export function logProperty(target: Object, propertyName: string): void {
// 获取对象属性的设计类型
var t = Reflect.getMetadata("design:type", target, propertyName);
console.log(`${propertyName} type: ${t.name}`);
}
class Greeter {
@logProperty
private name: string;
constructor(name: string) {
this.name = name;
}
greet(@logParameter message: string): string {
return `${this.name} says: ${message}`;
}
}
const greeter = new Greeter('jerome');
greeter.greet('hello');
design:type 表示被装饰的对象是什么类型
design:paramtypes 表示被装饰对象的参数类型
design:returntype 表示被装饰对象的返回值属性
运行这段代码:
$ node decorator/reflect-metadata.js
name type: String
indices => [ 0 ]
打印出了我们想知道的 name 的类型以及 message 的参数索引。
总结
typescript 的装饰器本质上提供了被装饰对象 Property Descriptor 的操作,都是在运行的时候被当作函数调用。看了这篇文章,是否觉得 typescript 这个装饰器的特性有点像 java spring 中注解的写法,我们可以利用我们写的装饰器来实现反射、依赖注入,类型断言等,实现在运行中程序对自身进行检查,vscode 就是典型的利用 typescript 的装饰器实现了依赖注入,有兴趣的请关注后续的文章。
扩展
装饰器是扩展 JavaScript 类的建议,TC39 五年来一直在研究装饰方案,有兴趣的可以关注下 TC39 的这个提案,babel 也实现了该提案,具体可查。