Эта статья — вторая часть серии:
- Часть 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.
В следующей статье мы изучим оставшийся тип (декоратор параметра), а также научимся создавать универсальные декораторы, которые можно применять к классам, свойствам, методам и параметрам.
ссылка на оригинал статьи https://habrahabr.ru/post/277321/
Добавить комментарий