Перейти к содержанию

Контекст вызова функции

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

Снимок advanced/lesson-33 является ES-модулем с коротким консольным опытом. Сеть и DOM здесь не нужны для объяснения. Программа не запускалась; ожидаемые сообщения показывают связь вызова и контекста. Не переносите правила из другого языка без проверки: запись метода в объекте ещё не прикрепляет функцию навсегда к одному экземпляру.

Получатель вызова

Определим объект читателя с темой и методом подписи:

const reader = {
  topic: 'frontend',
  describe() { return `Выбрана тема: ${this.topic}`; },
};
console.log(reader.describe());

Ожидается подпись фронтенда. В вызове reader.describe() объект слева от точки является получателем. Обычный метод получает его как this, поэтому читает reader.topic. Имя reader не написано внутри тела: та же функция может быть вызвана с другим получателем.

Если изменить поле reader.topic и снова вызвать метод, он прочитает новое состояние. Это динамическое чтение объекта, а не копия темы в момент определения. Поэтому результат зависит от получателя и его текущих данных. Такое свойство удобно для контроллера интерфейса, но должно быть ясно при передаче callback.

Сами функции являются значениями. Получение reader.describe возвращает функцию без её выполнения. Скобки запускают вызов. Эта разница была важна для регистрации событий в первом блоке, а теперь к ней добавляется вопрос получателя.

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

Потеря получателя

Продолжение полного опыта:

const detached = reader.describe;
try {
  console.log(detached());
} catch (error) {
  console.log(error.name);
}

Ожидается TypeError, потому что this равен undefined, а тело пытается прочитать его свойство. Присваивание в detached не переносит выражение reader.describe() как готовую связь. Оно сохраняет только ссылку на функцию. Поэтому похожий код может быть корректен при вызове методом и ошибочен после передачи отдельно.

В обработчиках браузера обычная функция иногда получает специальный this, связанный с объектом регистрации. Но это всё равно не обязано быть нашим объектом reader или контроллером. Не пытайтесь исправить проблему предположением, что всякая среда вызовет callback так же, как исходная точка в коде.

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

Привязка функции

bind возвращает новую функцию с закреплённым получателем:

const bound = reader.describe.bind(reader);
console.log(bound());

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

Для удаления слушателя нужно сохранить конкретную привязанную функцию. Если при регистрации выполнить method.bind(this), а при удалении снова написать то же выражение, получится другая функция. Одинаковый текст вызова bind не означает одинаковую идентичность созданного callback. Это прямое применение правил ссылок функций из предыдущих уроков.

call выполняет обычную функцию сразу с указанным получателем. В снимке последний опыт использует reader.describe.call({ topic: 'publishing' }): ожидается подпись публикации. Это полезно для понимания механизма, но в приложении не стоит подменять объект случайной структурой без контракта нужных полей.

Стрелочная функция и внешняя область

Стрелочная функция не создаёт собственное динамическое значение this; она использует его из окружающей области. Поэтому обёртка внутри метода или класса может сохранить нужный экземпляр, когда передаётся обработчику. В контроллере далее применим именно такой callback для формы.

Это не означает, что любой метод объекта надо записывать стрелкой. Если стрелочная функция определена как значение поля литерала на верхнем уровне модуля, окружающий this не станет автоматически самим литералом. Короткая форма и способ получения контекста — разные вопросы.

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

Следствия для контроллера

У будущего класса будут поля courses, selection и ссылки на DOM. Метод render прочитает их через this. Внутри обработчика формы стрелочная функция вызовет this.setSelection, сохранив экземпляр из метода регистрации. Важно увидеть эту связь, а не запомнить стрелку как универсальный способ исправить любой callback.

Если передать controller.render напрямую в событие без учёта контекста, получатель может оказаться неверным. Тогда ошибка не относится к данным курсов: они могут быть корректны, но функция читает их у другого объекта. Диагностика должна различать договор данных и договор вызова.

Для ручного наблюдения сравните четыре этапа снимка: метод, отделённая функция, привязанная функция и call с другой темой. Затем измените поле исходного объекта и предскажите, какие вызовы увидят изменение. Результаты пока не подтверждены запуском, поэтому сохраняйте программу как воспроизводимый опыт.

Функция, объект и момент чтения

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

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

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

При чтении кода ищите выражение вызова, а не место первоначального объявления функции. reader.describe() даёт один получатель, detached() — другой договор, а call явно задаёт получателя. Один и тот же текст тела не гарантирует одинаковый результат. Это позволяет предсказать проблему callback ещё до появления исключения в браузере.

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

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

Оглавление курса