Статьи

JavaScript-декораторы: что они и когда их используют

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

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

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

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

Это уже возможно в стандартном 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);

В этом примере создается новая функция — в переменной wrappeddoSomething Разница в том, что он будет делать некоторые записи до и после вызова функции-оболочки:

 doSomething('Graham');
// Hello, Graham

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

Как использовать JavaScript декораторы

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

Примечание: на момент написания, декораторы в настоящее время находятся в « Черновом этапе 2 », что означает, что они в основном закончены, но все еще могут быть изменены.

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

Например:

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

Это определяет класс и применяет три декоратора — два к самому классу и один к свойству класса:

  • @log
  • @immutableObject.freeze
  • @time

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

Буквально обновляем дескриптор свойства, чтобы установить для флага «доступный для записи» значение 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

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

Подняв его на ступеньку выше, мы можем организовать для нашего декоратора некоторые аргументы. Например, давайте перепишем наш декоратор 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;
  };
}

Сейчас это становится все сложнее, но когда мы разбиваем его, у нас есть это:

  • Функция logname
  • Затем эта функция возвращает функцию, которая сама является декоратором .

Это идентично более раннему декоратору logname

Это тогда используется следующим образом:

 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 выйдет из представленных аргументов и что построенное значение действительно является экземпляром 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, которая предоставляет некоторые очень полезные распространенные декораторы, которые готовы к использованию прямо сейчас. Как правило, они обеспечивают очень полезную общую функциональность (например, синхронизацию вызовов методов, предупреждения об устаревании, обеспечение того, что значение доступно только для чтения), но с использованием гораздо более чистого синтаксиса декоратора.

реагировать

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


Купите наш Премиум курс: React The ES6 Way


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

В общем, это будет использоваться следующим образом:

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

Однако из-за того, как работает синтаксис декоратора, его можно заменить следующим кодом для достижения точно такой же функциональности:

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

MobX

В библиотеке MobX широко используются декораторы, позволяющие легко помечать поля как наблюдаемые или вычисляемые, а классы — как наблюдатели.

Резюме

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

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