Mohan Ramоригинал, лицензияNew Frontendпереводить.
Декораторы позволяют программистам писать интроспективный код метаинформации. Лучший вариант использования декораторов — сквозные задачи — аспектно-ориентированное программирование.
Аспектно-ориентированное программирование (АОП)это парадигма программирования, которая позволяет нам разделитьСквозные проблемы, тем самым достигая цели повышения степени модульности. Он может добавлять дополнительное поведение (уведомления) к существующему коду, не изменяя сам код.
@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.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.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.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
, чтобы гарантировать, что при создании нового экземпляра Employeeinstanceof
Оператор работает как надо.
Когда новый конструктор готов, мы возвращаем его для завершения реализации конструктора класса.
После того, как новый конструктор готов, имя класса выводится на консоль каждый раз при создании экземпляра.
Скомпилированный код выглядит следующим образом.
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
.
С помощью отражения мы можем получить следующую информацию во время выполнения:
- организацияимя.
- организацияТипы.
- сущность реализованаинтерфейс.
- организацияПараметры конструктораимя и тип.
Эпилог
- декораторно ввремя разработкипомощьсамоанализкод,аннотацияи функции, которые изменяют классы и свойства.
- Иегуда Кац предложил добавить функцию декоратора в стандарт ECMAScript 2016:tc39/proposal-decorators
- мы можем пройтидекораторская фабрикаПередайте предоставленные пользователем параметры декоратору.
- Есть 4 декоратора:ДобрыйДекоратор,методдекоратор,свойство/доступдекоратор,параметрдекоратор.
- API отражения метаинформацииПомогает включить метаинформацию в объектах стандартным способом, а также вВремя выполненияПолучатьИнформация о типе дизайна.
Я помещаю все примеры кода в текстmohanramphp/typescript-decoratorsв этом репозитории Git. Спасибо за прочтение!
Заглавное изображение:Alex Loup