Как привязать методы класса к экземпляру класса с контекстом 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, все мы знаем, что он развивается семимильными шагами.