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

http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-ii
  • Перевод

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



В предыдущей статье мы выяснили, какие типы декораторов мы можем использовать в 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.


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

  • +14
  • 12,1k
  • 5
Онлайн-кинотеатр ivi
79,00
Компания
Поделиться публикацией

Комментарии 5

    0
    Использую сейчас декораторы для реализации статического конструктора класса. Может есть какой-то способ реализовать это без декораторов? А то легко забыть вызвать декоратор, а вставлять проверку при инстанцировании не хочется.
      0
      В js/ts можно нужный код просто написать непосредственно после объявления класса.
        0
        Да-да, вручную после каждого подкласса. Этого и хочется избежать.
          0
          Э… как подкласс и статический конструктор связаны? Если вы хотите вызывать некоторый код для каждого подкласса некоторого класса — то это уже называется "конструктор метакласса", а не "статический конструктор".
            0
            Называйте как хотите.

            class A  {
                foo = new String( 'Hello' )
                static bar = new String( 'World' )
            }
            
            class B extends A {}
            
            alert( ( new B ).foo + ' ' + B.bar ) // 'Hello World'
            alert( ( new A ).foo === ( new B ).foo ) // false
            alert( A.bar === B.bar ) // trur

            Cтатический конструктор отрабатывает лишь на корневом классе, а далее поля просто копируются. А хочется, чтобы конструктор отрабатывал на каждом субклассе, аналогично конструктору экземпляра.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое