/ JavaScript

Как привязать методы класса к экземпляру класса с контекстом this

Есть несколько способов обеспечить доступ к this в методах класса JavaScript. В этой статье мы быстро рассмотрим наиболее распространенные способы реализации этой задачи, обсудив преимущества и недостатки каждого из них.

Проблема появляется, когда у нас есть метод класса, похожий на этот:

class Logger {
  printName (name = 'some log message') {
    this.print(`Debug: ${name}`);
  }

  print (text) {
    console.log(text); 
  }
}

Затем, по какой-то причине - контекст метода printName меняется, ожидания не оправдываются, что приводит к ошибкам.

const logger = new Logger();
const { printName } = logger;
printName();
// <- Uncaught TypeError: Cannot read property 'print' of undefined

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

Способ появившийся ещё от пещерного человека

В этом сценарии мы просто вручную связываем все методы к экземпляром класса в самом конструкторе.

class Logger {
  constructor () {
    this.printName = this.printName.bind(this);
  }

  printName (name = 'some log message') {
    this.print(`Debug: ${name}`);
  }

  print (text) {
    console.log(text); 
  }
}

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

Автоматическая привязка

Похожий, но менее болезненный подход - использование модуля, который позаботится об этом от нашего имени. С помощью библиотеки auto-bind, которая обходит всё методы объекта и связывает их с текущим контекстом this.

class Logger {
  constructor () {
    autoBind(this);
  }

  printName (name = 'some log message') {
    this.print(`Debug: ${name}`);
  }

  print (text) {
    console.log(text); 
  }
}

Этот подход хорошо работает для классов, хотя нам, как и раньше, не обойтись без конструктора. Преимуществом этого метода является то, что нам не нужно отслеживать каждый метод по имени для их привязки. В то же время, если мы имеем дело с объектами, а не классами, нам необходимо убедиться, что autoBind вызывается для объекта после того, как каждый метод был назначен объекту, иначе некоторые методы останутся непривязанными. Любые методы, добавленные после вызова autoBind, являются несвязанными, а это означает, что в некоторых ситуациях autoBind является ещё худшим вариантом, чем ручной вызов .bind для каждого метода.

Прокси

Объект Proxy может использоваться для перехвата get-операций (геттер), в котором можно возвращать методы, привязанные к классу. Ниже у нас есть функция selfish, которая принимает объект и возвращает прокси для этого объекта. Любые методы, доступ к которым осуществляется через прокси, будут автоматически привязаны к объекту. WeakMap используется, чтобы гарантировать, что мы связываем методы только один раз, так что равенство в proxy.fn === proxy.fn сохраняется.

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}
const logger = selfish(new Logger());

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

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

Но все эти варианты определенно лучше, чем старый способ, когда нам приходилось писать Logger.prototype.print. Приватная область видимость для классов, в которой вы можете объявлять функции, привязанные к этому классу, которые не являются методами класса (но доступны для каждого метода), и другие свойства, привязанные к классу, были бы огромным шагом вперед для развития семантики классов.

Наличие менее подробной, утомительной, подверженной ошибкам или сложной семантики привязки контекста this - это, в основном, вопрос времени. Что касается JavaScript, все мы знаем, что он развивается семимильными шагами.