Учитывая введение стандарта ES2015+, и то, что транспиляция в наше время — обычное дело, многие программисты сталкиваются с новыми возможностями JavaScript в реальном коде и в учебных материалах. Одна из таких возможностей — декораторы. Во всех этих новшествах немудрено и запутаться, поэтому сегодня поговорим о том, что такое декораторы, и о том, как их использовать для того, чтобы сделать код чище и понятнее.

image

Декораторы обрели популярность благодаря их применению в Angular 2+. В Angular этот функционал реализуется средствами TypeScript. Сейчас предложение по введению декораторов в JavaScript находится в состоянии Stage 2 Draft. Это означает, что работа над ними, в основном, завершена, но они всё ещё могут подвергаться изменениям. Декораторы должны стать частью следующего обновления языка.

Что такое декоратор?


В простейшем виде декоратор — это способ оборачивания одного фрагмента кода в другой. Буквально — «декорирование» фрагмента кода.

Об этой концепции вы, возможно, слышали ранее, как о «Функциональной композиции» или о «Функциях высшего порядка».

Подобное вполне реализуемо стандартными средствами JavaScript. Выглядит это как вызов некоей функции, которая оборачивает другую:

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

В этом примере показано, как создаётся новая функция, которая назначается константе wrapped. Эта функция может быть вызвана точно так же, как и функция doSomething, и делать она будет то же самое. Разница заключается в том, что до и после вызова оборачиваемой функции будет выполнено логирование. Вот что произойдёт, если поэкспериментировать с функциями doSomething и wrapped.

doSomething('Graham');
// Hello, Graham
wrapped('Graham');
// Starting
// Hello, Graham
// Finished

Как применять декораторы в JavaScript?


Декораторы в JavaScript используют специальный синтаксис, они имеют префикс в виде символа @, их размещают непосредственно перед кодом, который хотят декорировать.

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

Например:

@log()
@immutable()
class Example {
  @time('demo')
  doSomething() {
  }
}

Здесь показано объявление класса и применение трёх декораторов. Два из них относятся к самому классу, и один — к свойству класса. Вот каковы роли этих декораторов:

  • @log может логировать все обращения к классу.
  • @immutable способен сделать класс иммутабельным — возможно, он вызовет Object.freeze для новых экземпляров класса.
  • @time записывает сведения о длительности исполнения методов и выводит эти сведения в лог с уникальным тегом.

Сегодня использование декораторов требует применение транспилятора, так как их пока не поддерживают ни браузеры, ни Node.js.

Если вы используете Babel, для работы с декораторами можно обратиться к плагину transform-decorators-legacy.

Обратите внимание на то, что в названии этого плагина используется слово «legacy», которое можно трактовать как указание на некую устаревшую технологию. Дело тут в том, что здесь поддерживается то, как декораторы обрабатывает Babel 5. Этот подход может отличаться от той формы, которая, в итоге, будет стандартизирована.

Зачем нужны декораторы?


Функциональная композиция в JavaScript без особых проблем реализуется и стандартными средствами. Однако, тот же подход либо очень сложно, либо невозможно применить к другим программным конструкциям, например — к классам и их свойствам. Предложенные нововведения позволяют использовать декораторы и с классами, и с их свойствами. Вероятно, в будущих версиях JavaScript можно ожидать дальнейшего развития декораторов.

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

Разные типы декораторов


Сейчас поддерживаемые типы декораторов — это декораторы классов и членов классов — свойств, методов, геттеров и сеттеров. На самом деле, декораторы — это всего лишь функции, которые вызываются с некоторыми сведениями о декорируемых элементах и возвращают другие функции.

Декораторы выполняются один раз при запуске программы, декорируемый код заменяется возвращаемым значением.

?Декораторы членов класса


Декораторы членов класса применяются к свойствам, методам, геттерам и сеттерам.

Эти функции-декораторы вызываются с тремя параметрами:

  • target — класс, в котором находится декорируемый член класса.
  • name — имя члена класса.
  • descriptor — дескриптор члена класса. Это, по существу, объект, который был бы передан методу Object.defineProperty.

Вот классический пример, который демонстрирует использование декоратора @readonly. Этот декоратор реализован так:

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

Декоратор устанавливает флаг writable дескриптора свойства в значение false.

Затем этот декоратор используется с членами класса следующим образом:

class Example {
  a() {}
  @readonly
  b() {}
}
 
const e = new Example();
e.a = 1;
e.b = 2;
// TypeError: Cannot assign to read only property 'b' of object '#<Example>'

Как видно по сообщению об ошибке, декоратор сработал так, как ожидается. Мы можем пойти и дальше, например, изменив поведение декорируемой функции, фактически, заменив её новой функцией. Давайте, с помощью декоратора, выведем в лог аргументы функции, и то, что она возвращает:

function log(target, name, descriptor) {
  const original = descriptor.value;
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log(`Arguments: ${args}`);
      try {
        const result = original.apply(this, args);
        console.log(`Result: ${result}`);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}

Эта конструкция заменяет метод новым, который логирует аргументы, вызывает исходный метод, а затем логирует то, что он возвращает.

Обратите внимание на то, что здесь мы использовали оператор расширения для того, чтобы автоматически создать массив со всеми переданными методу аргументами. Это — современная альтернатива свойству функции arguments.

Посмотрим на всё это в действии:

class Example {
    @log
    sum(a, b) {
        return a + b;
    }
}

const e = new Example();
e.sum(1, 2);
// Arguments: 1,2
// Result: 3

Можно заметить, что в тексте функции-декоратора приходится использовать необычный синтаксис для исполнения декорируемого метода. Об этом, на самом деле, можно написать целую статью, но, если кратко, то метод apply позволяет вызывать функцию, задавая значение this и аргументы, которые будут ей переданы.

Пойдя ещё дальше, можно устроить всё так, чтобы декоратор принимал аргументы для собственных целей. Например, перепишем декоратор log следующим образом:

function log(name) {
  return function decorator(t, n, descriptor) {
    const original = descriptor.value;
    if (typeof original === 'function') {
      descriptor.value = function(...args) {
        console.log(`Arguments for ${name}: ${args}`);
        try {
          const result = original.apply(this, args);
          console.log(`Result from ${name}: ${result}`);
          return result;
        } catch (e) {
          console.log(`Error from ${name}: ${e}`);
          throw e;
        }
      }
    }
    return descriptor;
  };
}

Код усложнился, но если с ним разобраться, окажется, что происходит тут следующее:

  • Имеется функция log, которая принимает единственный аргумент — name.
  • Эта функция возвращает ещё одну функцию, которая и является декоратором.

Возвращённая функция идентична декоратору log, который мы описывали выше, за исключением того, что она использует параметр name из внешней функции.

Пользоваться всем этим можно так:

class Example {
  @log('some tag')
  sum(a, b) {
    return a + b;
  }
}

const e = new Example();
e.sum(1, 2);
// Arguments for some tag: 1,2
// Result from some tag: 3

Сразу видно, что такой подход позволяет различать строки логов благодаря назначенному тегу.

Здесь выполняется вызов функции вида log('some tag'), а затем то, что было возвращено из этого вызова, используется как декоратор для метода sum.

?Декораторы классов


Декораторы классов применяются ко всему определению класса. Функция-декоратор вызывается с единственным параметром, которым является декорируемая функция-конструктор класса.

Обратите внимание на то, что декоратор применяется к функции-конструктору, а не к каждому экземпляру класса в момент его создания. Поэтому для того, чтобы по-разному декорировать разные экземпляры одного и того же класса, понадобится, перед их созданием, самостоятельно заботиться о декорировании конструктора.

В целом, декораторы классов менее полезны, чем декораторы членов классов, так как всё, что тут можно сделать, сводится к замене конструктора класса.

Возвращаясь к примеру с логированием, напишем декоратор, который будет выводить параметры конструктора:

function log(Class) {
  return (...args) => {
    console.log(args);
    return new Class(...args);
  };
}

Тут, в качестве аргумента, принимается класс и возвращается новая функция, которая будет действовать как конструктор. В нашем случае она просто логирует аргументы и возвращает новый экземпляр класса, созданного с этими аргументами.

Например:

@log
class Example {
  constructor(name, age) {
  }
}

const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}

Как видно, при выполнении конструктора класса Example будет выполнено логирование аргументов конструктора, которые используются при создании экземпляра этого класса. Это — именно то, чего мы добивались.

Для передачи параметров декораторам классов можно воспользоваться уже описанным подходом:

function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: args`);
      return new Class(...args);
    };
  }
}

@log('Demo')
class Example {
  constructor(name, age) {}
}

const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}

Декораторы в реальных проектах


Вот несколько примеров использования декораторов в популярных библиотеках.

?Библиотека Core Decorators


Есть отличная библиотека Core Decorators, которая предоставляет готовые к использованию декораторы общего назначения. Среди поддерживаемой ими функциональности — тайминг вызовов методов, уведомления об устаревших конструкциях, проверка того, является ли некий объект неизменяемым.

?Библиотека React


В React найдено хорошее применение концепции компонентов высшего порядка. Это — компоненты React, написанные как функции и служащие обёртками для других компонентов.

Они — идеальные кандидаты на использование в качестве декораторов, так как для того, чтобы использовать их в таком качестве, потребуются минимальные усилия. Например, в библиотеке Redux есть функция connect, которая используется для подключения компонентов React. Без декораторов работа с этой функцией может выглядеть так:

class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

Если же переписать это с использованием декораторов, получится следующее:

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

Функционал получился тот же, но выглядит всё это гораздо симпатичнее.

?Библиотека MobX


Декораторы широко используются в библиотеке MobX. Например, для обеспечения нужного поведения системы надо просто добавлять к полям декораторы Observable или Computed, а с классами использовать декоратор Observers.

Итоги


Мы поговорили о том, как создавать и использовать декораторы в JavaScript, в частности — рассмотрели особенности работы с декораторами членов класса. Такой подход позволяет писать вспомогательный код, представленный функциями-декораторами, который можно применять для изменения поведения методов различных классов. Синтаксис декораторов позволяет упростить тексты программ, сделать их чище и понятнее. С декораторами в JavaScript можно работать уже сегодня, они нашли применение в популярных библиотеках. Однако, полагаем, после того, как их напрямую будут поддерживать браузеры и Node.js, у них найдётся множество новых поклонников.

Уважаемые читатели! А вы уже пользуетесь декораторами в своих JS-проектах?
Поделиться с друзьями
-->

Комментарии (22)


  1. ReklatsMasters
    13.06.2017 16:15

    А как в примере с реактом экспортировать недекорированную функцию? Для тестов, например.


    1. justboris
      13.06.2017 22:18

      Видимо, никак. Поэтому декораторы нечасто используются вместе с connect из react-redux.


      1. RV170
        14.06.2017 12:50

        С реактом удобно использовать recompose для таких целей


  1. YaakovTooth
    13.06.2017 16:43
    +1

    Одна из главных фич сахарных фич питона, которую очень и очень хотелось в JS.


  1. megaboich
    13.06.2017 16:49

    Еще декораторы могут буть удобны для реализации IoC не только для Angular. Есть уже неплохая реализация: InversifyJS


  1. inoyakaigor
    13.06.2017 18:14
    +1

    Фича интересная, но есть один неудобный нюанс в применении её к реакту и react devtools:

    @logger 
    class SomeClass extends Component {}
    

    в devtools даст название компонента logger вместо ожидаемого SomeClass. Если код чужой, то потребуется дополнительное время чтобы разобраться, что это вообще за компонент.
    А так да, фича довольно-таки удобная


    1. ReklatsMasters
      13.06.2017 18:38

      Думаю нет, декоратор же будет как stateless компонент, а значит в иерархии покажется и компонент logger, и как дочерний SomeClass.


    1. mayorovp
      13.06.2017 19:24

      Надо в таких случаях переопределять name у внутренней функции.


      1. TheShock
        13.06.2017 23:50

        Оно ведь readonly. Я когда-то для корректного имени классов в devtools делал очень жесткий костыль. Работало очень клево, даже имена с точкой допускались:

        // <debug>
        if (params.name) {
        	Constructor = new Function('con', 'return {"' + params.name + '": ' +
        		function(){ return con.apply(this, arguments) }
        	 + '}["' + params.name + '"];')(Constructor);
        }
        // </debug>
        



      1. Ghost_nsk
        14.06.2017 12:59
        -2

        В таких случая надо использовать прокси


        1. mayorovp
          14.06.2017 13:52

          И как же это поможет?


          1. Ghost_nsk
            14.06.2017 14:20

            магическим образом поможет в devtools видеть нормальные названия


            1. mayorovp
              14.06.2017 14:22

              Пока что не вижу связи. Не могли бы вы пояснить детали реализации?


              1. Ghost_nsk
                14.06.2017 14:41
                +1

                Идея в том что бы вместо такого подхода

                function loggingDecorator(wrapped) {
                    return function() {/* log & apply here */}
                }
                wrapped = loggingDecorator(wrapped);
                

                использовать
                wrapped = new Proxy(wrapped, {apply{/* log & apply here */}});
                


                1. mayorovp
                  14.06.2017 14:51

                  Хм, и правда, так тоже можно. Но этот способ будет медленным.


    1. MingONE
      14.06.2017 12:50

      Вот так можно задать любое имя для отображения в девтулсах

      function withSubscription(WrappedComponent) {
        class WithSubscription extends React.Component {/* ... */}
        WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
        return WithSubscription;
      }
      
      function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
      }
      


      https://facebook.github.io/react/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging


  1. Radiosterne
    14.06.2017 13:06
    +1

    Декоратор observer во-первых называется без s на конце, во-вторых находится в библиотеке mobx-react, а не mobx, и в-третьих применяется не к «классам», а к React-компонентам, оборачивая их метод render в autorun — который в свою очередь и является одной из стандартных утилит для подписки на изменение observable'ов в mobx.


    1. mayorovp
      14.06.2017 13:56

      Ну, с autorun вы загнули. Там не простая обертка, а микс-ин на полтораста строк. С остальным я согласен.


      1. Radiosterne
        14.06.2017 15:34

        Суть миксина примерно аналогична оборачиванию render в autorun, и даже документация mobx-react предлагает думать об этом именно в таком ключе :) Но да, ваша правда, как минимум там внутри Reaction для более тонкого управления вызовами.


        1. mayorovp
          14.06.2017 15:46

          Не совсем. render — это обычная чистая функция, в autorun ее оборачивать нет ни малейшего смысла. Reaction там внутрях для того, чтобы обнаружив изменения правильно пнуть react-dom для того чтобы он вызвал, собственно, рендер.


          1. Radiosterne
            14.06.2017 15:50

            Ай, верно!


  1. VolCh
    14.06.2017 17:03

    Если вы используете Babel, для работы с декораторами можно обратиться к плагину transform-decorators-legacy.

    Обратите внимание на то, что в названии этого плагина используется слово «legacy», которое можно трактовать как указание на некую устаревшую технологию. Дело тут в том, что здесь поддерживается то, как декораторы обрабатывает Babel 5. Этот подход может отличаться от той формы, которая, в итоге, будет стандартизирована.

    Скорее дело в том, что этот плагин обрабатывает декораторы несколько по иному по сравнению с тем видом, который сейчас специфицирован в Stage 2. Независимо от того, будет ли спека меняться дальше или нет, актуальной реализации на сегодняшний день нет. Нужно понимать, что даже если спека будет утверждена в текущем виде, то код, работающий с transform-decorators-legacy может оказаться неработающим на нативных реализациях декораторов.