Эта статья — вторая часть серии:
- Часть 1: Декораторы методов
- Часть 2: Декораторы свойств и классов
- Часть 3: Декораторы параметров и фабрика декораторов
- Часть 4: Сериализация типов и metadata reflection API
В предыдущей статье мы выяснили, какие типы декораторов мы можем использовать в TypeScript.
Мы также узнали, как реализовать декоратор метода и ответили на основные вопросы про то, как декораторы работают в TypeScript:
- Как они вызываются?
- Кто передает в них аргументы?
- Где объявлена функция
__decorate
?
В этой статье мы познакомимся с двумя новыми типами декораторов: декоратором свойства (PropertyDecorator
) и декоратором класса (ClassDecorator
).
Декоратор свойства
Мы уже знаем, что сигнатура декоратора свойства выглядит так:
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
Мы можем использовать декоратор свойства logProperty
следующим образом:
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
}
Если скомпилировать этот код в JavaScript, мы обнаружим, что в нем вызывается функция __decorate
(с которой мы разбирались в первой части), но на этот раз у нее не хватает последнего параметра (дескриптора свойства, полученного через Object.getOwnPropertyDescriptor
)
var Person = (function () {
function Person(name, surname) {
this.name = name;
this.surname = surname;
}
__decorate([
logProperty
], Person.prototype, "name");
return Person;
})();
Декоратор получает 2 аргумента (прототип и ключ), а не 3 (прототип, ключ и дескриптор свойства), как в случае с декоратором метода.
Другой важный момент: на этот раз компилятор TypeScript не использует значение, возвращаемое функцией __decorate
для того, чтобы переопредилить оригинальное свойство, как это было с декоратором метода:
Object.defineProperty(C.prototype, "foo",
__decorate([
log
],
C.prototype,
"foo",
Object.getOwnPropertyDescriptor(C.prototype, "foo")
)
);
Теперь, когда мы знаем, что декоратор свойства принимает прототип декорируемого класса и имя декорируемого поля в качестве аргументов и ничего не возвращает, давайте реализуем logProperty
:
function logProperty(target: any, key: string) {
// значение свойства
var _val = this[key];
// геттер для свойства
var getter = function () {
console.log(`Get: ${key} => ${_val}`);
return _val;
};
// сеттер для свойства
var setter = function (newVal) {
console.log(`Set: ${key} => ${newVal}`);
_val = newVal;
};
// Удаляем то, что уже находится в поле
if (delete this[key]) {
// Создаем новое поле с геттером и сеттером
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
Декоратор выше объявляет переменную с именем _val
и сохраняет в нее значение декорируемого свойства (так как this
в данном контексте указывает на прототип класса, а key
— на название свойства).
Далее, объявляются функции getter
(используется для получение значения свойства) и setter
(используется для установки значение свойства). Обе функции имеют доступ к _val
благодаря замыканиям, созданным при их объявлении. Именно здесь мы добавляем дополнительное поведение к свойству, в
данном случае — вывод строчки в лог при изменении значения свойства.
Затем, оператор delete
используется для того, чтобы удалить исходное свойство из прототипа класса.
Обратите внимание, что оператор delete
бросает исключение в "строгом режиме", если удаляемое поле — собственное неконфигурируемое свойство (в обычном режиме возвращается false
).
Если удаление прошло успешно, метод Object.defineProperty()
используется для того, чтобы создать новое свойство с исходным именем, но на этот раз оно использует объявленные ранее функции getter
и setter
.
Теперь декоратор будет выводить в консоль изменения свойства каждый раз, когда мы получаем или устанавливаем в него значение.
var me = new Person("Remo", "Jansen");
// Set: name => Remo
me.name = "Remo H.";
// Set: name => Remo H.
me.name;
// Get: name Remo H.
Декоратор класса
Как нам уже известно, сигнатура декоратора класса выглядит следующим образом:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
Мы можем использовать декоратор с именем logClass
так:
@logClass
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
}
После компиляции в JavaScript вызывается функция __decorate
, и на этот раз у нее нет уже двух последних аргументов:
var Person = (function () {
function Person(name, surname) {
this.name = name;
this.surname = surname;
}
Person = __decorate([
logClass
], Person);
return Person;
})();
Обратим внимание на то, что компилятор передает в __decorate
Person
, а не Person.prototype
.
Кроме того, заметим, что компилятор использует возвращаемое значение для того, чтобы переопределить исходный конструктор.
Person = __decorate(/* ... */);
Запомним, что декоратор класса должен возвращать функцию-конструктор.
Теперь мы можем реализовать logClass
:
function logClass(target: any) {
// сохраняем ссылку на исходный конструктор
var original = target;
// вспомогательная функция для генерации экземпляров класса
function construct(constructor, args) {
var c : any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}
// новое поведение конструктора
var f : any = function (...args) {
console.log("New: " + original.name);
return construct(original, args);
}
// копируем прототип, чтобы работал оператор instanceof
f.prototype = original.prototype;
// возвращаем новый конструктор (он переопределит исходный)
return f;
}
Декоратор выше создает переменную original
и сохраняет в нее конструктор декорируемого класса.
Далее объявляется вспомогательная функция construct
, которая позволит нам создавать экземпляры класса.
Затем мы создаем переменную f
, которая будет использоваться как новый конструктор. Она вызывает исходный конструктор, а также логирует в консоль название инстанцируемого класса. Именно здесь мы добавляем новое поведение к исходному классу.
Протоип исходного конструктора копируется в прототип f
, благодаря чему оператор instanceof
работает с объектами Person
.
Остается просто вернуть новый конструктор, и наша реализация готова.
Теперь декоратор будет выводить в консоль имя класса каждый раз, когда он инстанцируется:
var me = new Person("Remo", "Jansen");
// New: Person
me instanceof Person;
// true
Заключение
Теперь у нас есть глубокое понимание того, как работают 3 из 4 типов декораторов в TypeScript.
В следующей статье мы изучим оставшийся тип (декоратор параметра), а также научимся создавать универсальные декораторы, которые можно применять к классам, свойствам, методам и параметрам.