- PVSM.RU - https://www.pvsm.ru -

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

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

  • Часть 1: Декораторы методов [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 (с которой мы разбирались в первой части [1]), но на этот раз у нее не хватает последнего параметра (дескриптора свойства, полученного через 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.

В следующей статье мы изучим оставшийся тип (декоратор параметра), а также научимся создавать универсальные декораторы, которые можно применять к классам, свойствам, методам и параметрам.

Автор: онлайн-кинотеатр ivi.ru

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/112465

Ссылки в тексте:

[1] Часть 1: Декораторы методов: https://habrahabr.ru/company/ivi/blog/275003

[2] Источник: https://habrahabr.ru/post/277321/