Декораторы и рефлексия в TypeScript: от новичка до эксперта (ч.2)

от автора


Эта статья — вторая часть серии:

  • Часть 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *