使用 TypeScript 装饰器装饰代码
Mohan Ram 原作,授权 New Frontend 翻译。
装饰器让程序员可以编写元信息以内省代码。装饰器的最佳使用场景是横切关注点——面向切面编程。
面向切面编程(AOP) 是一种编程范式,它允许我们分离横切关注点,藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)。
@log // 类装饰器
class Person {
constructor(private firstName: string, private lastName: string) {}
@log // 方法装饰器
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const person = new Person('Mohan', 'Ram');
person.getFullName();
上面的代码展示了装饰器多么具有声明性。下面我们将介绍装饰器的细节:
- 什么是装饰器?它的目的和类型
- 装饰器的签名
- 方法装饰器
- 属性装饰器
- 参数装饰器
- 访问器装饰器
- 类装饰器
- 装饰器工厂
- 元信息反射 API
- 结语
什么是装饰器?它的目的和类型
装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。
装饰器使用 @expression
的形式,其中 expression
必须能够演算为在运行时调用的函数,其中包括装饰声明信息。
它起到了以声明式方法将元信息添加至已有代码的作用。
装饰器类型及其执行优先级为
- 类装饰器——优先级 4 (对象实例化,静态)
- 方法装饰器——优先级 2 (对象实例化,静态)
- 访问器或属性装饰器——优先级 3 (对象实例化,静态)
- 参数装饰器——优先级 1 (对象实例化,静态)
注意,如果装饰器应用于类构造函数的参数,那么不同装饰器的优先级为:1. 参数装饰器,2. 方法装饰器,3. 访问器或参数装饰器,4. 构造器参数装饰器,5. 类装饰器。
// 这是一个装饰器工厂——有助于将用户参数传给装饰器声明
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
我们看到,上面的代码中,f
和 g
返回了另一个函数(装饰器函数)。f
和 g
称为装饰器工厂。
装饰器工厂 帮助用户传递可供装饰器利用的参数。
我们还可以看到,演算顺序为由顶向下,执行顺序为由底向上。
装饰器的签名
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;
方法装饰器
从上面的签名中,我们可以看到方法装饰器函数有三个参数:
-
target —— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是
Employee.prototype
- propertyKey —— 方法的名称
-
descriptor —— 方法的属性描述符,即
Object.getOwnPropertyDescriptor(Employee.prototype, propertyKey)
export function logMethod(
target: Object,
propertyName: string,
propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
// target === Employee.prototype
// propertyName === "greet"
// propertyDesciptor === Object.getOwnPropertyDescriptor(Employee.prototype, "greet")
const method = propertyDesciptor.value;
propertyDesciptor.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 propertyDesciptor;
};
class Employee {
constructor(private firstName: string, private lastName: string
) {}
@logMethod
greet(message: string): string {
return `${this.firstName} ${this.lastName} says: ${message}`;
}
}
const emp = new Employee('Mohan Ram', 'Ratnakumar');
emp.greet('hello');
上面的代码应该算是自解释的——让我们看看编译后的 JavaScript 是什么样的。
"use strict";
var __decorate = (this && this.__decorate) ||
function (decorators, target, key, desc) {
// 函数参数长度
var c = arguments.length
/**
* 处理结果
* 如果仅仅传入了装饰器数组和目标,那么应该是个类装饰器。
* 否则,如果描述符(第 4 个参数)为 null,就根据已知值准备属性描述符,
* 反之则使用同一描述符。
*/
var r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc;
// 声明存储装饰器的变量
var 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]) {
/**
* 如果仅仅传入了装饰器数组和目标,那么应该是类装饰器,
* 传入目标调用装饰器。
* 否则,如果 4 个参数俱全,那么应该是方法装饰器,
* 据此进行调用。
* 反之则使用同一描述符。
* 如果传入了 3 个参数,那么应该是属性装饰器,可进行相应的调用。
* 如果以上条件皆不满足,返回处理的结果。
*/
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
}
}
};
/**
* 由于只有方法装饰器需要根据应用装饰器的结果修正其属性,
* 所以最后返回处理好的 r
*/
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Employee = /** @class */ (function () {
function Employee(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Employee.prototype.greet = function (message) {
return this.firstName + " " + this.lastName + " says: " + message;
};
// typescript 调用 `__decorate` 辅助函数,
// 以便在对象原型上应用装饰器
__decorate([
logMethod
], Employee.prototype, "greet");
return Employee;
}());
var emp = new Employee('Mohan Ram', 'Ratnakumar');
emp.greet('hello');
让我们开始分析 Employee 函数——构造器初始化 name
参数和 greet
方法,将其加入原型。
__decorate([logMethod], Employee.prototype, "greet");
这是 TypeScript 自动生成的通用方法,它根据装饰器类型和相应参数处理装饰器函数调用。
该函数有助于内省方法调用,并为开发者铺平了处理类似日志、记忆化、应用配置等横切关注点的道路。
在这个例子中,我们仅仅打印了函数调用及其参数、响应。
注意,阅读 __decorate
方法中的详细注释可以理解其内部机制。
属性装饰器
属性装饰器函数有两个参数:
-
target —— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是
Employee.prototype
- propertyKey —— 属性的名称
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 Employee {
@logParameter
name: string;
}
const emp = new Employee();
emp.name = 'Mohan Ram';
console.log(emp.name);
// Set: name => Mohan Ram
// Get: name => Mohan Ram
// Mohan Ram
上面的代码中,我们在装饰器中内省属性的可访问性。下面是编译后的代码。
var Employee = /** @class */ (function () {
function Employee() {
}
__decorate([
logParameter
], Employee.prototype, "name");
return Employee;
}());
var emp = new Employee();
emp.name = 'Mohan Ram'; // Set: name => Mohan Ram
console.log(emp.name); // Get: name => Mohan Ram
参数装饰器
参数装饰器函数有三个参数:
-
target —— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是
Employee.prototype
- 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];
}
}
class Employee {
greet(@logParameter message: string): string {
return `hello ${message}`;
}
}
const emp = new Employee();
emp.greet('hello');
在上面的代码中,我们收集了所有被装饰的方法参数的索引或位置,作为元数据加入对象的原型。下面是编译后的代码。
// 返回接受参数索引和装饰器的函数
var __param = (this && this.__param) || function (paramIndex, decorator) {
// 该函数返回装饰器
return function (target, key) { decorator(target, key, paramIndex); }
};
var Employee = /** @class */ (function () {
function Employee() {}
Employee.prototype.greet = function (message) {
return "hello " + message;
};
__decorate([
__param(0, logParameter)
], Employee.prototype, "greet");
return Employee;
}());
var emp = new Employee();
emp.greet('hello');
类似之前见过的 __decorate
函数,__param
函数返回一个封装参数装饰器的装饰器。
如我们所见,调用参数装饰器时,会忽略其返回值。这意味着,调用 __param
函数时,其返回值不会用来覆盖参数值。
这就是参数装饰器不返回的原因所在。
访问器装饰器
访问器不过是类声明中属性的读取访问器和写入访问器。
访问器装饰器应用于访问器的属性描述符,可用于观测、修改、替换访问器的定义。
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 Employee {
private _salary: number;
private _name: string;
@enumerable(false)
get salary() { return `Rs. ${this._salary}`; }
set salary(salary: any) { this._salary = +salary; }
@enumerable(true)
get name() {
return `Sir/Madam, ${this._name}`;
}
set name(name: string) {
this._name = name;
}
}
const emp = new Employee();
emp.salary = 1000;
for (let prop in emp) {
console.log(`enumerable property = ${prop}`);
}
// salary 属性不在清单上,因为我们将其设为假
// output:
// decorator - sets the enumeration part of the accessor
// decorator - sets the enumeration part of the accessor
// enumerable property = _salary
// enumerable property = name
上面的例子中,我们定义了两个访问器 name
和 salary
,并通过装饰器设置是否将其列入清单,据此决定对象的行为。name
将列入清单,而 salary
不会。
注意:TypeScript 不允许同时装饰单一成员的 get
和 set
访问器。相反,所有成员的装饰器都必须应用于首个指定的访问器(根据文档顺序)。这是因为装饰器应用于属性描述符,属性描述符结合了 get
和 set
访问器,而不是分别应用于每项声明。
下面是编译的代码。
function enumerable(value) {
return function (target, propertyKey, descriptor) {
console.log('decorator - sets the enumeration part of the accessor');
descriptor.enumerable = value;
};
}
var Employee = /** @class */ (function () {
function Employee() {
}
Object.defineProperty(Employee.prototype, "salary", {
get: function () { return "Rs. " + this._salary; },
set: function (salary) { this._salary = +salary; },
enumerable: true,
configurable: true
});
Object.defineProperty(Employee.prototype, "name", {
get: function () {
return "Sir/Madam, " + this._name;
},
set: function (name) {
this._name = name;
},
enumerable: true,
configurable: true
});
__decorate([
enumerable(false)
], Employee.prototype, "salary", null);
__decorate([
enumerable(true)
], Employee.prototype, "name", null);
return Employee;
}());
var emp = new Employee();
emp.salary = 1000;
for (var prop in emp) {
console.log("enumerable property = " + prop);
}
类装饰器
类装饰器应用于类的构造器,可用于观测、修改、替换类定义。
export function logClass(target: Function) {
// 保存一份原构造器的引用
const original = target;
// 生成类的实例的辅助函数
function construct(constructor, args) {
const c: any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}
// 新构造器行为
const f: any = function (...args) {
console.log(`New: ${original['name']} is created`);
return construct(original, args);
}
// 复制 prototype 属性,保持 intanceof 操作符可用
f.prototype = original.prototype;
// 返回新构造器(将覆盖原构造器)
return f;
}
@logClass
class Employee {}
let emp = new Employee();
console.log('emp instanceof Employee');
console.log(emp instanceof Employee); // true
上面的装饰器声明了一个名为 original
的变量,将其值设为被装饰的类构造器。
接着声明了名为 construct
的辅助函数。该函数用于创建类的实例。
我们接下来创建了一个名为 f
的变量,该变量将用作新构造器。该函数调用原构造器,同时在控制台打印实例化的类名。这正是我们给原构造器加入额外行为的地方。
原构造器的原型复制到 f
,以确保创建一个 Employee 新实例的时候,instanceof
操作符的效果符合预期。
新构造器一旦就绪,我们便返回它,以完成类构造器的实现。
新构造器就绪之后,每次创建实例时会在控制台打印类名。
编译后的代码如下。
var Employee = /** @class */ (function () {
function Employee() {
}
Employee = __decorate([
logClass
], Employee);
return Employee;
}());
var emp = new Employee();
console.log('emp instanceof Employee');
console.log(emp instanceof Employee);
在编译后的代码中,我们注意到两处不同:
- 如你所见,传给
__decorate
的参数有两个,装饰器数组和构造器函数。 - TypeScript 编译器使用
__decorate
的返回值以覆盖原构造器。
这正是类装饰器必须返回一个构造函数的原因所在。
装饰器工厂
由于每种装饰器都有它自身的调用签名,我们可以使用装饰器工厂来泛化装饰器调用。
import { logClass } from './class-decorator';
import { logMethod } from './method-decorator';
import { logProperty } from './property-decorator';
import { logParameter } from './parameter-decorator';
// 装饰器工厂,根据传入的参数调用相应的装饰器
export function log(...args) {
switch (args.length) {
case 3: // 可能是方法装饰器或参数装饰器
// 如果第三个参数是数字,那么它是索引,所以这是参数装饰器
if typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
case 2: // 属性装饰器
return logProperty.apply(this, args);
case 1: // 类装饰器
return logClass.apply(this, args);
default: // 参数数目不合法
throw new Error('Not a valid decorator');
}
}
@log
class Employee {
@log
private name: string;
constructor(name: string) {
this.name = name;
}
@log
greet(@log message: string): string {
return `${this.name} says: ${message}`;
}
}
元信息反射 API
元信息反射 API (例如 Reflect
)能够用来以标准方式组织元信息。
「反射」的意思是代码可以侦测同一系统中的其他代码(或其自身)。
反射在组合/依赖注入、运行时类型断言、测试等使用场景下很有用。
import "reflect-metadata";
// 参数装饰器使用反射 api 存储被装饰参数的索引
export function logParameter(target: Object, propertyName: string, index: number) {
// 获取目标对象的元信息
const indices = Reflect.getMetadata(`log_${propertyName}_parameters`, target, propertyName) || [];
indices.push(index);
// 定义目标对象的元信息
Reflect.defineMetadata(`log_${propertyName}_parameters`, indices, target, propertyName);
}
// 属性装饰器使用反射 api 获取属性的运行时类型
export function logProperty(target: Object, propertyName: string): void {
// 获取对象属性的设计类型
var t = Reflect.getMetadata("design:type", target, propertyName);
console.log(`${propertyName} type: ${t.name}`); // name type: String
}
class Employee {
@logProperty
private name: string;
constructor(name: string) {
this.name = name;
}
greet(@logParameter message: string): string {
return `${this.name} says: ${message}`;
}
}
上面的代码用到了 reflect-metadata 这个库。其中,我们使用了反射元信息的设计键(例如:design:type
)。目前只有三个:
-
类型元信息用了元信息键
design:type
。 -
参数类型元信息用了元信息键
design:paramtypes
。 -
返回类型元信息用了元信息键
design:returntype
。
有了反射,我们就能够在运行时得到以下信息:
- 实体名。
- 实体类型。
- 实体实现的接口。
- 实体构造器参数的名称和类型。
结语
- 装饰器 不过是在设计时(design time)帮助内省代码,注解及修改类和属性的函数。
- Yehuda Katz 提议在 ECMAScript 2016 标准中加入装饰器特性:tc39/proposal-decorators
- 我们可以通过装饰器工厂将用户提供的参数传给装饰器。
- 有 4 种装饰器:类装饰器、方法装饰器、属性/访问器装饰器、参数装饰器。
- 元信息反射 API 有助于以标准方式在对象中加入元信息,以及在运行时获取设计类型信息。
我把文中所有代码示例都放到了 mohanramphp/typescript-decorators 这个 Git 仓库中。谢谢阅读!
题图:Alex Loup
其他内容推荐
- 6 款热门 macOS 开源应用
- 22 个 iOS 开发热门开源项目
- 你可能不知道的 npm 实用技巧
推荐阅读
-
如何使用 @Builder 装饰器优化 HarmonyOS NEXT 中 UI 组件的重复使用?
-
香港 NEXT 如何使用 @Styles 装饰器优化组件代码?-第 1 步:定义全局 @Styles 方法
-
Humboldt NEXT 如何使用 @Styles 装饰器优化组件代码?
-
探索 Python 装饰器的新领域:包装库的神秘力量
-
Java 8新特性探究(十三)JavaFX 8新特性以及开发2048游戏-JavaFX历史## 跟java在服务器端和web端成绩相比,桌面一直是java的软肋,于是Sun公司在2008年推出JavaFX,弥补桌面软件的缺陷,请看下图JavaFX一路走过来的改进 从上图看出,一开始推出时候,开发者需使用一种名为JavaFX Script的静态的、声明式的编程语言来开发JavaFX应用程序。因为JavaFX Script将会被编译为Java bytecode,程序员可以使用Java代码代替。 JavaFX 2.0之后的版本摒弃了JavaFX Script语言,而作为一个Java API来使用。因此使用JavaFX平台实现的应用程序将直接通过标准Java代码来实现。 JavaFX 2.0 包含非常丰富的 UI 控件、图形和多媒体特性用于简化可视化应用的开发,WebView可直接在应用中嵌入网页;另外 2.0 版本允许使用 FXML 进行 UI 定义,这是一个脚本化基于 XML 的标识语言。 从JDK 7u6开始,JavaFx就与JDK捆绑在一起了,JavaFX团队称,下一个版本将是8.0,目前所有的工作都已经围绕8.0库进行。这是因为JavaFX将捆绑在Java 8中,因此该团队决定跳过几个版本号,迎头赶上Java 8。 ##JavaFx8的新特性 ## ###全新现代主题:Modena 新的Modena主题来替换原来的Caspian主题。不过在Application的start方法中,可以通过setUserAgentStylesheet(STYLESHEET_CASPIAN)来继续使用Caspian主题。 参考http://fxexperience.com/2013/03/modena-theme-update/ ###JavaFX 3D 在JavaFX8中提供了3D图像处理API,包括Shape3D (Box, Cylinder, MeshView, Sphere子类),SubScene, Material, PickResult, LightBase (AmbientLight 和PointLight子类),SceneAntialiasing等。Camera类也得到了更新。从JavaDoc中可以找到更多信息。 ###富文本 强化了富文本的支持 ###TreeTableView ###日期控件DatePicker 增加日期控件 ###用于 CSS 结构的公共 API
-
如何使用牛顿或L-BFGS优化器来实现最大熵模型的伪代码: L-BFGS_optimal_lasso_pseudo_code
-
探索函数对象、函数嵌套、名称空间、作用域和装饰器
-
玩转JS版设计模式(11):揭秘JavaScript中的装饰器模式
-
Vue项目中的装饰器模式简介
-
深入理解装饰者模式:如何运用设计模式提升代码效率