装饰器让程序员可以编写元信息以内省代码。装饰器的最佳使用场景是横切关注点——面向切面编程。
面向切面编程(AOP) 是一种编程范式,它允许我们分离横切关注点,藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知)。
@log // 类装饰器
class Person {
constructor(private firstName: string, private lastName: string) {}
@log // 方法装饰器
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
什么是装饰器?它的目的和类型
装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。
装饰器使用@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 称为装饰器工厂。
装饰器工厂 帮助用户传递可供装饰器利用的参数。
我们还可以看到,演算顺序为由顶向下,执行顺序为由底向上。
方法装饰器
方法装饰器函数有三个参数:
target
—— 当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 Employee.prototypepropertyKey
—— 方法的名称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');
属性装饰器
属性装饰器函数有两个参数:
- 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.prototypepropertyKey
—— 参数的名称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');
访问器装饰器
访问器不过是类声明中属性的读取访问器和写入访问器。
访问器装饰器应用于访问器的属性描述符,可用于观测、修改、替换访问器的定义
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 不会。
类装饰器
类装饰器应用于类的构造器,可用于观测、修改、替换类定义。
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 操作符的效果符合预期。
新构造器一旦就绪,我们便返回它,以完成类构造器的实现。
新构造器就绪之后,每次创建实例时会在控制台打印类名。
结尾
- 装饰器 不过是在设计时(design time)帮助内省代码,注解及修改类和属性的函数。
- Yehuda Katz 提议在 ECMAScript 2016 标准中加入装饰器特性:tc39/proposal-decorators
- 我们可以通过装饰器工厂将用户提供的参数传给装饰器。
- 有 4 种装饰器:类装饰器、方法装饰器、属性/访问器装饰器、参数装饰器。
- 元信息反射 API 有助于以标准方式在对象中加入元信息,以及在运行时获取设计类型信息。