Pull to refresh

Разбираем декораторы ES2016

Reading time 8 min
Views 99K


Многие из нас, наверное, уже устали от этой шумихи вокруг последних стандартов ECMAScript. ES6, ES7 ECMAScript Harmony… Кажется, что у каждого свое мнение на счет того, как правильно называть JS. Но даже несмотря на весь этот хайп, то что сейчас происходит с JavaScript — это самое замечательное, что происходило с ним за последние лет 5 минимум. Язык живет, развивается, комьюнити постоянно предлагает новые возможности и синтаксические конструкции. Одной из таких новых конструкций, безусловно заслуживающих внимания, являются декораторы. Занявшись поисками материалов по этой теме, я понял, что в русскоязычном интернете практически ничего нет о декораторах. В то же время Addy Osmani еще в июле 2015 представил прекрасную статью Exploring ES2016 Decorators на Medium. В связи с этим, я хотел бы представить вашему вниманию перевод этой статьи на русский язык и разместить его здесь.

Итераторы, генераторы, списковые включения… С каждым нововведением отличий между JavaScript и Python становится все меньше. Сегодня мы поговорим еще об одном «питоноподопном» предложении для стандарта ECMAScript 2016 (он же ES7) — Декораторы от Иегуды Кац.

Паттерн «Декоратор»


Прежде чем разбирать область применения и правила использования декораторов для ES2016, давайте все-таки узнаем есть ли что-то подобное в других языках? Например в Python. Для него декораторы — это просто синтаксический сахар, который позволяет упрощенно вызывать функции высшего порядка. Иными словами это просто функция, которая принимает другую функцию, изменяет ее поведение, при этом не внося изменений в ее исходный код. Простейший декоратор мы могли бы представить как показано ниже:

@mydecorator
def myfunc():
  pass

Выражение в самом верху примера ("@mydecorator") — это декоратор, синтаксис которого точно так же будет выглядеть и в стандарте ES2016 (ES7), так что часть необходимого материала вы уже освоили.

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

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

Декораторы в ES5 и ES2015 (он же ES6)


Реализовать декоратор в ES5, используя императивный подход (то есть при помощи чистых функций) — достаточно тривиальная задача. Но из-за того что в ES2015 появилась нативная поддержка классов, для обеспечения тех же целей нам нужно что-то более гибкое, чем просто чистые функции.

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

Давайте же рассмотрим некоторые декораторы ES2016 на реальном примере!

ES2016 декораторы в действии


Итак, вспомним чему мы научились из Python и попробуем перенести наши знания в область JavaScript. Как я уже сказал, синтаксис для ES7 ничем не будет отличаться, основная идея тоже. Таким образом, декоратор в ES2016 — это некоторое выражение, которое может принимать целевой объект, имя и дескриптор в качестве аргументов. Затем вы просто «применяете» его добавляя символ "@" в начале и размещаете прямо перед тем участком кода, который вы собираетесь декорировать. На сегодняшний день декораторы могут быть определены либо для определения класса, либо для определения свойства.

Декорируем свойство


Давайте рассмотрим класс:

class Cat {
  meow() { return `${this.name} says Meow!`}
}

При добавлении этого метода к прототипу класса Cat мы получим что-то похожее на это:

  Object.defineProperty(Cat.prototype, 'meow', {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
  });

Таким образом на прототип класса вешается метод, позволяющий коту подавать голос. В то же время этот метод не участвует в переборе свойств объекта, также его можно изменять и он доступен для записи. Но представьте, что вы хотели бы явно запретить изменения реализации этого метода. Например, мы могли бы сделать это при помощи декоратора "@readonly", который, как понятно из названия, давал бы доступ к этому методу «только для чтения» и запрещал бы любые переопределения:

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

И затем добавляем созданный декоратор к методу, как показано ниже:

  class Cat {
    @readonly
    meow() { return `${this.name} says Meow!`}
  }

Декоратор также может принимать параметры, в конечном итоге декоратор — это всего лишь выражение, поэтому как "@readonly", так и "@something(parameter)" должны работать.

Теперь прежде чем повесить метод на прототип класса Cat, движок запустит декоратор:

  let descriptor = {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
  };

  // Декоратор имеет ту же сигнатуру, что и "Object.defineProperty",
  // Поэтому мы без труда можем переопределить дескриптор, пока 
  // "Object.defineProperty" не вызван
  descriptor = readonly(Cat.prototype, 'meow', descriptor) || descriptor;
  Object.defineProperty(Cat.prototype, 'meow', descriptor); 

Таким образом декорируемый метод «meow» становится доступным только для чтения. Можем проверить это поведение:

  let garfield = new Cat();
  garfield.meow = () => { console.log('I want lasagne!'); };

  // Ошибка! Так как мы не можем изменять реализацию метода.

Занятно, не правда ли? Совсем скоро мы рассмотрим декорирование классов, но перед этим давайте поговорим о поддержке декораторов со стороны комьюнити. Несмотря на свою незрелость, начинают появляться целые библиотеки декораторов (например, https://github.com/jayphelps/core-decorators.js от Jay Phelps). Подобно тому, как мы написали свой декоратор, можно взять точно такой же, но реализованный в библиотеке:

  import { readonly } from 'core-decorators';

  class Meal {
    @readonly
    entree = 'steak';
  }

  let meal = new Meal();
  meal.entree = 'salmon';

  // Ошибка!

Библиотека также хороша тем, что реализует декоратор "@deprecate", довольно полезный, когда вы меняете API, но необходимо сохранить устаревшие методы для обратной совместимости. Вот что говорит документация касательно этого декоратора:
Декоратор вызывает console.warn(), и выводит предупреждающее сообщение. Это сообщение можно переопределять. Также поддерживает добавление ссылок для дальнейшего ознакомления.


  import { deprecate } from 'core-decorators';
  
  class Person {
    @deprecate
    facepalm() {}

    @deprecate('We are stopped facepalming.')
    facepalmHard() {}

    @deprecate('We are stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
    facepalmHarder() {}
  }

  let captainPicard = new Person();

  captainPicard.facepalm();
  // DEPRECATION Person#facepalm will be removed in future versions

  captainPicard.facepalmHard();
  // DEPRECATION Person#facepalmHard: We are stopped facepalming.

  
  captainPicard.facepalmHarder();
  // DEPRECATION Person#facepalmHarder: We are stopped facepalming.
  //
  // See http://knowyourmemes.com/memes/facepalm for more details
  //

Декорируем класс


Далее, давайте посмотрим на декорирование классов. Согласно предлагаемой спецификации, декоратор принимает целевой конструктор. Например, мы могли бы создать декоратор superhero для класса «MySuperHero»:

function superhero(target) {
  target.isSuperhero = true;
  target.power = 'flight';
}

@superhero
class MySuperHero {}

console.log(MySuperHero.isSuperhero);      // true

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

function superhero(isSuperhero) {
  return function (target) {
    target.isSuperhero = isSuperHero;
    target.power = 'flight';
  }
  
}

@superhero(false)
class MySuperHero {}

console.log(MySuperHero.isSuperhero);      // false

Итак, декораторы в ES2016 могут работать с классами и дескрипторами свойств. И, как мы уже выяснили, им автоматически передаются дескриптор свойства и целевой объект. Имея доступ к дескриптору свойства, декоратор может делать такие вещи как: добавление геттера к свойству, добавление поведения, которое могло бы выглядеть громоздко без вынесения его в декоратор, например автоматическая смена контекста вызова функции на текущую сущность при первой попытке доступа к свойству.

ES2016 декораторы и миксины


Я тщательно ознакомился со статьей Рега Брейтуэйта Декораторы ES2016 в качестве миксинов и его предыдущей Функциональные миксины и вынес оттуда довольно интересный вариант использования декораторов. Его предложение заключается в том, что мы вводим некий helper, который подмешивает поведение к прототипу или же объекту определенного класса. При этом функциональный миксин, который смешивает поведение объекта с прототипом класса выглядит так:

  function mixin(behaviour, sharedBehaviour = {}) {
    const instanceKeys = Reflect.ownKeys(behaviour);
    const sharedKeys = Reflect.ownKeys(sharedBehaviour);
    const typeTag = Symbol(‘isa’);
   
   function _mixin(clazz) {
      for (let property of instanceKeys) {
        Object.defineProperty(clazz.prototype, property, { value: behaviour[property] });
      }
      Object.defineProperty(clazz.prototype, typeTag, { value: true});
      return clazz;
    }
    for(let property of sharedKeys) {
      Object.defineProperty(_mixin, property, {
        value: sharedBehaviour[property],
        enumerable: sharedBehaviour.propertyIsEnumerable(property)
      });
    }
    Object.defineProperty(_mixin, Symbol.hasInstance, {
      value: (i) => !!i[typeTag]
    });
    return _mixin;
  }

Отлично. Теперь мы можем определить несколько миксинов и попробовать декорировать ими класс. Давайте представим, что у нас есть класс “ComicBookCharacter”:

  class ComicBookCharacter {
    constructor(first, last) {
      this.firstName = first;
      this.lastName = last;
    }

    realName() {
      return this.firstName + ‘ ‘ + this.lastName;
    }
  }

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

  const SuperPowers = mixin({
    addPower(name) {
      this.powers().push(name);
      return this;
    },
    powers() {
      return this._powers_pocessed || (this._powers_pocessed = []);
    }
  });

  const UtilityBelt = mixin({
    addToBelt(name) {
      this.utilities().push(name);
      return this;
    },
    utilties() {
      return this._utility_items || (this._utility_items = []);
    }
  });

Теперь мы можем использовать @-синтаксис с именами наших миксинов, для того, чтобы декорировать объявленный выше класс “ComicBookCharacter”, добавляя в него желаемое поведение. Обратите внимание, мы можем определять несколько инструкций декорирования вместе:

  @SuperPowers
  @UtilityBelt
  class ComicBookCharacter {
    constructor(first, last) {
      this.firstName = first;
      this.lastName = last;
    }
    realName() {
      return this.firstName + ‘ ‘ + this.lastName;
    }
  }

Теперь мы можем создать своего Бэтмена:

  const batman = new ComicBookCharacter(‘Bruce’, ‘Wayne’);
  console.log(batman.realName());
  // Bruce Wayne

  batman
      .addToBelt(‘batarang’)
      .addToBelt(‘cape’);

  console.log(batman.utilities());
  // [‘batarang’, ‘cape’]

  batman
       .addPower(‘detective’)
       .addPower(‘voice sounds like Gollum has asthma’);

  console.log(batman.powers());
  // [‘detective’, ‘voice sounds like Gollum has asthma’]

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

Заставляем Babel понимать декораторы


Декораторы (на момент написания статьи) все еще не утверждены и только предложены к добавлению в стандарт. Но благодаря тому, что Babel поддерживает транспиляцию экспериментальных конструкций, мы можем подружить его с декораторами.

Если вы используете Babel CLI, вы можете включить декораторы так:
  babel --optional es7.decorators

Или же вы можете включить поддержку декораторов при помощи трасформатора:

  babel.transform(‘code’, { optional: [‘es7.decorators’] });

Ну и в конце-концов можете поиграться с ними в Babel REPL (для этого включите поддержку экспериментальных конструкций).

Почему вы еще не используете декораторы в ваших проектах?


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

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

Полезные ссылки


https://github.com/wycats/javascript-decorators
https://github.com/jayphelps/core-decorators.js”>https://github.com/jayphelps/core-decorators.js
http://blog.developsuperpowers.com/eli5-ecmascript-7-decorators/
http://elmasse.github.io/js/decorators-bindings-es7.html
http://raganwald.com/2015/06/26/decorators-in-es7.html”>http://raganwald.com/2015/06/26/decorators-in-es7.html
Tags:
Hubs:
+13
Comments 20
Comments Comments 20

Articles