Объект события: target, type, currentTarget

события≈ 6 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Обработчик получает не пустой вызов, а объект события — досье на то, что случилось: где щёлкнули, какое событие произошло и когда. Разберём поля, без которых не обходится ни один обработчик: target, currentTarget, type.

Разделы темы:·

Браузер вызывает обработчик с одним аргументом — объектом события. Назвать его можно как угодно: event, e, ev. Это обычный параметр функции, имя ни на что не влияет, важен только порядок — он там всегда первый и всегда единственный.

Внутри — свойства, описывающие произошедшее, и все они только для чтения. У клика это ещё и координаты с номером кнопки мыши, у клавиатуры — код клавиши. Но четыре поля есть у любого события, от click до самодельного: type, target, currentTarget, timeStamp.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Последняя строка — не украшение. Мы не ждём, пока кто-то ткнёт в кнопку: go.click() порождает событие программно, и обработчик отрабатывает тут же, синхронно. Дальше все примеры устроены так же.

target — самый глубокий элемент, на котором событие возникло. currentTarget — элемент, на котором висит обработчик, выполняющийся прямо сейчас. Событие всплывает от target вверх по предкам, и на каждом слушателе currentTarget свой, а target всю дорогу один и тот же. Сам маршрут — погружение, цель, всплытие — разбирает статья «Всплытие и погружение»; здесь важно только, что слушателей по дороге бывает несколько.

Формула короткая: target — «где щёлкнули», currentTarget — «кто слушает». В обработчике-function тот же элемент лежит ещё и в this, но у стрелочной функции своего this нет, поэтому привыкайте писать event.currentTarget — это работает в обоих случаях.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ссылка в currentTarget живёт ровно столько, сколько идёт обход дерева. Как только обработчик вернул управление, браузер обнуляет поле — и в отложенном коде вместо элемента оказывается null. Лечится одной строкой: сохранить нужный элемент в переменную сразу, пока он ещё на месте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

type — строка с именем события: click, input, keydown. Она нужна, когда одна и та же функция подписана на несколько типов сразу: разобрать поведение по event.type проще, чем держать три почти одинаковых обработчика.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Список из двадцати пунктов, и на каждый нужен клик. Можно навесить двадцать обработчиков, а можно один — на общего родителя — и внутри посмотреть на target. Это делегирование, и держится оно ровно на разнице между target и currentTarget: слушаем родителя, а работаем с тем, куда попали. Здесь приём показан ради этой разницы; как на нём держатся панели, списки и формы — в статье «Делегирование событий».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ловушка здесь одна, зато встречается у всех: target — самый глубокий узел, а не тот элемент, который вы держали в голове. Появилась внутри пункта иконка или span — и проверка event.target.tagName === "LI" перестаёт срабатывать. Поэтому от target всегда поднимаются вверх через closest().

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Делегирование бессильно с событиями, которые не всплывают: focus, blur, load. У первых двух есть всплывающие двойники focusin и focusout, а в общем случае помогает третий аргумент addEventListener со стадией перехвата: { capture: true }.

Кроме четвёрки основных полей в объекте события есть несколько служебных. Знать их наизусть не нужно, но узнавать в чужом коде полезно.

ПолеЧто в нём
event.typeИмя события строкой: click, input, keydown
event.targetЭлемент, на котором событие возникло
event.currentTargetЭлемент, чей обработчик выполняется сейчас
event.timeStampМиллисекунды от старта страницы до события
event.bubblesВсплывает ли событие вверх по дереву
event.cancelableМожно ли отменить действие по умолчанию
event.defaultPreventedВызывали ли уже preventDefault()
event.isTrustedtrue — сделал человек, false — породил код

Настоящие пользовательские события всплывают и обычно отменяемы, а вот у самодельного new Event("input") оба флага по умолчанию выключены. Из-за этого делегирование «не видит» рукотворные события, пока не передашь вторым аргументом { bubbles: true }. Кстати, у события из element.click() поле isTrusted равно false — браузер честно помечает всё, что пришло не от человека.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

  • Обработчик получает объект события первым и единственным аргументом; имя переменной любое, поля только для чтения.
  • target — где событие возникло, currentTarget — чей обработчик выполняется сейчас: при всплытии target неизменен, currentTarget меняется на каждом уровне.
  • currentTarget обнуляется сразу после обработчика — в setTimeout и промисах там уже null, поэтому элемент сохраняют в переменную.
  • type позволяет одной функции обслуживать несколько типов событий, а timeStamp полезен разницей между двумя событиями, а не абсолютным значением.
  • Делегирование — это один обработчик на родителе плюс event.target.closest(селектор); работает только для всплывающих событий.

1/3Обработчик клика висит на div#card. Внутри него button, внутри кнопки span, по которому и щёлкнули. Что окажется в event.target?

2/3Внутри обработчика вызвали setTimeout, а в его колбэке читают event.target и event.currentTarget. Что там будет?

3/3Один обработчик на ul, внутри каждого li лежит span с текстом. Почему проверка event.target.tagName === "LI" ненадёжна?

Одна панель — один обработчик

Кнопок на панели со временем станет больше, вешать обработчик на каждую не хочется. Сделайте один обработчик на контейнере: он печатает data-action нажатой кнопки, а на клик мимо кнопок молчит.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Слушаем контейнер, а от target поднимаемся к кнопке через closest(). Если кнопки над точкой клика нет — тихо выходим.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка if (!button) return; обязательна: клик по самому контейнеру тоже попадает в обработчик, и там closest() вернёт null. Приятный бонус: кнопки можно добавлять и удалять в любой момент — обработчик про них ничего не знает, переподписывать его не нужно.

Кнопка, которая не узнаёт себя

Код должен печатать id кнопки, а печатает id иконки внутри неё — потому что клик попал в span. Почините обработчик, поменяв ровно одно слово.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Нужен элемент, на котором висит обработчик, а не точка клика — то есть currentTarget.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вариант event.target.closest("button").id тоже вернёт «like», но он лишний: обработчик и так знает свой элемент. А this.id здесь не спасёт — у стрелочной функции своего this нет.