Статьи

Классов и функций со стрелками: контекст поучительной истории

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

Поспешный переподготовка

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

let outerThis, tfeThis, afeThis;
let obj = {
  outer() {
    outerThis = this;

    traditionalFE = function() {tfeThis = this};
    traditionalFE();

    arrowFE = () => afeThis = this;
    arrowFE();
  }
}
obj.outer();

outerThis; // obj
tfeThis; // global
afeThis; // obj
outerThis === afeThis; // true

Функции стрелок и классы

Учитывая безошибочный подход функции arrow к контексту, заманчиво использовать его вместо методов в классах. Рассмотрим этот простой класс, который подавляет все клики в данном контейнере и сообщает о DOM-узле, чье событие click было подавлено:

class ClickSuppresser {
  constructor(domNode) {
    this.container = domNode;
    this.initialize();
  }

  suppressClick(e) {
    e.preventDefault();
    e.stopPropagation();
    this.clickSuppressed(e);
  }

  clickSuppressed(e) {
    console.log('click suppressed on', e.target);
  }

  initialize() {
    this.container.addEventListener(
      'click', this.suppressClick.bind(this));
  }
}

В этой реализации используется сокращенный синтаксис метода ES6. Мы должны привязать прослушиватель событий к текущему экземпляру (строка 18), иначе thisзначение в suppressClickбудет узлом контейнера.

Использование функций стрелок вместо синтаксиса метода избавляет от необходимости связывать обработчик:

class ClickSuppresser {
  constructor(domNode) {
    this.container = domNode;
    this.initialize();
  }

  suppressClick = e => {
    e.preventDefault();
    e.stopPropagation();
    this.clickSuppressed(e);
  }

  clickSuppressed = e => {
    console.log('click suppressed on', e.target);
  }

  initialize = () => {
    this.container.addEventListener(
      'click', this.suppressClick);
  }
}

Отлично!

Но подождите, что это?

ClickSuppresser.prototype.suppressClick; // undefined
ClickSuppresser.prototype.clickSuppressed; // undefined
ClickSuppresser.prototype.initialize; // undefined

Почему функции не были добавлены в прототип?

Оказывается, проблема не столько в самой функции стрелки, сколько в том, как она туда попадает. Функции со стрелками — это не методы, а анонимные выражения функций, поэтому единственный способ добавить их в класс — это присвоить свойству. А классы ES обрабатывают методы и свойства совершенно по-разному.

Методы добавляются в прототип класса, где мы их и хотим — это означает, что они определены только один раз, а не один раз для каждого экземпляра. Напротив, синтаксис свойств класса (который на момент написания является предложением-кандидатом в ES7² ) является просто сахаром для назначения одинаковых свойств каждому экземпляру. В сущности, свойства класса работают так:

class ClickSuppresser {
  constructor(domNode) {

    this.suppressClick = e => {...}
    this.clickSuppressed = e => {...}
    this.initialize = e => {...}

    this.node = domNode;
    this.initialize();
  }
}

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

const cs1 = new ClickSuppresser();
const cs2 = new ClickSuppresser();

cs1.suppressClick === cs2.suppressClick; // false
cs1.clickSuppressed === cs2.clickSuppressed; // false
cs1.initialize === cs2.initialize; // false

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

… в котором (сладкая ирония) функции стрелки приходят на помощь

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

Тем не менее, ни один дракон не может быть убит. Мы можем заменить bindпредыдущую функцию на функцию стрелки.

initialize() {
  this.container.addEventListener(
    'click', e => this.suppressClick(e));
}

Почему это работает? Так suppressClickкак определяется с использованием обычного синтаксиса метода, он получит контекст экземпляра, который его вызвал ( thisв примере выше). И поскольку функции со стрелками имеют лексическую область видимости, thisэто будет текущий экземпляр нашего класса.

Если вы не хотите каждый раз искать аргументы, вы можете воспользоваться оператором rest / spread:

initialize() {
  this.container.addEventListener(
    'click', (...args) => this.suppressClick(...args));
}

Заворачивать

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

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