Пошук уроків, статей та іншого контенту
Визначите правила this у DOM-обробниках подій і навчитеся зберігати потрібний контекст у браузері.
this в обробнику DOM-подіїУ браузері значення this залежить не від місця оголошення функції, а від способу її виклику.
Для звичайної функції, переданої в addEventListener, браузер викликає її приблизно так:
listener.call(element, event);Тому в обробнику звичайної функції:
button.addEventListener("click", function (event) {
console.log(this === event.currentTarget); // true
});this посилається на елемент, на якому зареєстровано цей обробник.
Це правило діє для звичайних функцій:
const button = document.querySelector("button");
button.addEventListener("click", function (event) {
console.log(this); // <button>
console.log(event.currentTarget); // <button>
});У більшості випадків event.currentTarget є явнішим і безпечнішим способом отримати елемент, для якого виконується поточний обробник.
target і currentTargetПід час поширення події потрібно розрізняти дві властивості:
event.target — елемент, на якому подія фактично виникла;
event.currentTarget — елемент, чий обробник зараз виконується.
Розглянемо делегування подій:
<ul id="menu">
<li><button data-action="save">Зберегти</button></li>
<li><button data-action="delete">Видалити</button></li>
</ul>
<script>
const menu = document.querySelector("#menu");
menu.addEventListener("click", function (event) {
console.log(event.target);
console.log(event.currentTarget);
console.log(this === event.currentTarget); // true
});
</script>Якщо натиснути кнопку:
event.target буде кнопкою;
event.currentTarget і this будуть елементом ul.
Тому в делегованому обробнику не можна використовувати this як елемент, на якому відбулася дія:
menu.addEventListener("click", function (event) {
// this — це ul, а не натиснута кнопка
const button = event.target.closest("button");
if (!button) {
return;
}
console.log(button.dataset.action);
});thisСтрілкова функція не створює власного this. Вона використовує this із зовнішньої області видимості.
Тому в такому коді this не буде кнопкою:
button.addEventListener("click", (event) => {
console.log(this); // this із зовнішнього контексту
});У модулі браузера або в суворому режимі це зазвичай буде undefined. В іншому контексті значення може бути іншим, але браузер не підставляє в стрілкову функцію елемент події.
Якщо потрібен елемент події, використовуйте:
button.addEventListener("click", (event) => {
console.log(event.currentTarget);
});Стрілкові функції зручні, коли потрібно зберегти this зовнішнього об’єкта або екземпляра класу:
class Counter {
constructor(element) {
this.element = element;
this.value = 0;
element.addEventListener("click", () => {
this.value += 1;
this.render();
});
}
render() {
this.element.textContent = `Натискань: ${this.value}`;
}
}У стрілковій функції this успадковується з конструктора, де this є екземпляром Counter.
Вибір залежить від того, яке значення this потрібне:
button.addEventListener("click", function (event) {
// this === button
this.classList.toggle("active");
});button.addEventListener("click", (event) => {
// this не є button
event.currentTarget.classList.toggle("active");
});Для обробників елементів часто краще явно використовувати event.currentTarget. Це робить код менш залежним від типу функції:
button.addEventListener("click", (event) => {
event.currentTarget.disabled = true;
});Передавання методу без прив’язки контексту часто призводить до втрати this:
const controller = {
count: 0,
handleClick() {
this.count += 1;
console.log(this.count);
}
};
button.addEventListener("click", controller.handleClick);Під час виклику браузер передасть у this сам button, а не controller. У результаті метод працюватиме не з тим об’єктом, для якого був написаний.
Потрібно прив’язати метод:
button.addEventListener("click", controller.handleClick.bind(controller));Але такий спосіб має важливий недолік: результат bind() — це нова функція. Якщо її не зберегти, пізніше буде складно видалити обробник.
bind() і видалення обробникаremoveEventListener() видаляє обробник лише тоді, коли отримує ту саму функцію, яку було передано в addEventListener().
Цей код не спрацює:
button.addEventListener("click", controller.handleClick.bind(controller));
// Створюється інша bound-функція
button.removeEventListener("click", controller.handleClick.bind(controller));Правильний варіант:
const boundHandler = controller.handleClick.bind(controller);
button.addEventListener("click", boundHandler);
button.removeEventListener("click", boundHandler);У класах посилання на прив’язаний обробник зазвичай зберігають у властивості:
class Dialog {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener("click", this.handleClick);
}
handleClick(event) {
console.log("Діалог:", this.element);
}
destroy() {
this.element.removeEventListener("click", this.handleClick);
}
}Після bind() метод стає окремою функцією, але її this завжди посилається на потрібний екземпляр.
Інший спосіб зберегти контекст — стрілкова функція як властивість класу:
class Dialog {
handleClick = (event) => {
console.log(this.element);
};
constructor(element) {
this.element = element;
element.addEventListener("click", this.handleClick);
}
destroy() {
this.element.removeEventListener("click", this.handleClick);
}
}Така властивість створюється окремо для кожного екземпляра. Її this лексично прив’язаний до екземпляра.
Переваги:
не потрібно викликати bind();
посилання на функцію доступне для removeEventListener();
зручно передавати метод як callback.
Недолік:
кожен екземпляр має власну копію функції;
для великої кількості об’єктів це може збільшити використання пам’яті.
event.currentTarget доступний лише під час обробки подіїcurrentTarget описує поточний етап поширення події. Після завершення обробника браузер може встановити його в null.
button.addEventListener("click", function (event) {
console.log(event.currentTarget); // button
setTimeout(() => {
console.log(event.currentTarget); // зазвичай null
}, 0);
});Якщо елемент потрібен в асинхронному коді, збережіть його заздалегідь:
button.addEventListener("click", function (event) {
const element = event.currentTarget;
setTimeout(() => {
element.classList.add("processed");
}, 500);
});Те саме стосується this: звичайна функція всередині setTimeout() не отримає this зовнішнього обробника.
button.addEventListener("click", function () {
setTimeout(function () {
console.log(this); // не button
}, 0);
});Для збереження контексту можна використати стрілкову функцію:
button.addEventListener("click", function () {
setTimeout(() => {
console.log(this); // button
}, 0);
});Але для читабельності часто краще зберігати потрібне значення явно:
button.addEventListener("click", function (event) {
const buttonElement = event.currentTarget;
setTimeout(() => {
buttonElement.disabled = true;
}, 1000);
});Подія проходить кілька етапів:
захоплення — від window до цільового елемента;
обробка на цільовому елементі;
спливання — від цільового елемента назад до window.
На кожному етапі currentTarget відповідає елементу, обробник якого зараз виконується.
const outer = document.querySelector("#outer");
const inner = document.querySelector("#inner");
outer.addEventListener(
"click",
function (event) {
console.log("Захоплення:", this.id, event.target.id);
},
true
);
inner.addEventListener("click", function (event) {
console.log("Ціль:", this.id, event.target.id);
});
outer.addEventListener("click", function (event) {
console.log("Спливання:", this.id, event.target.id);
});Якщо натиснути #inner, результат буде приблизно таким:
Захоплення: outer inner
Ціль: inner inner
Спливання: outer innerevent.target не змінюється під час поширення, а this і event.currentTarget змінюються відповідно до поточного обробника.
Делегування дає змогу зареєструвати один обробник на спільному контейнері:
<ul id="tasks">
<li>
Вивчити <button data-action="complete">готово</button>
</li>
<li>
Повторити <button data-action="remove">видалити</button>
</li>
</ul>
<script>
const tasks = document.querySelector("#tasks");
tasks.addEventListener("click", (event) => {
const actionButton = event.target.closest("[data-action]");
if (!actionButton || !tasks.contains(actionButton)) {
return;
}
const action = actionButton.dataset.action;
const item = actionButton.closest("li");
if (action === "complete") {
item.classList.toggle("completed");
}
if (action === "remove") {
item.remove();
}
});
</script>У цьому прикладі:
this не використовується, бо обробник є стрілковою функцією;
контейнер зберігається в tasks;
потрібна кнопка знаходиться через event.target.closest(...);
перевірка tasks.contains(actionButton) не дає обробляти елементи з іншої частини документа.
closest() особливо важливий, коли всередині кнопки є вкладений елемент:
<button data-action="save">
<span>Зберегти</span>
</button>У такому випадку event.target може бути span, а не button.
handleEventaddEventListener() може отримати не лише функцію, а й об’єкт із методом handleEvent.
class MenuController {
constructor(menu) {
this.menu = menu;
menu.addEventListener("click", this);
}
handleEvent(event) {
const button = event.target.closest("button");
if (!button || !this.menu.contains(button)) {
return;
}
console.log(`Дія: ${button.dataset.action}`);
}
destroy() {
this.menu.removeEventListener("click", this);
}
}У цьому випадку:
браузер викликає controller.handleEvent(event);
всередині handleEvent() this посилається на об’єкт MenuController;
event.currentTarget посилається на menu;
об’єкт можна передати в removeEventListener() тим самим посиланням.
Цей підхід корисний для складних контролерів із кількома типами подій.
class FormController {
constructor(form) {
this.form = form;
form.addEventListener("submit", this);
form.addEventListener("input", this);
}
handleEvent(event) {
if (event.type === "submit") {
this.handleSubmit(event);
}
if (event.type === "input") {
this.handleInput(event);
}
}
handleSubmit(event) {
event.preventDefault();
console.log("Форму відправлено");
}
handleInput(event) {
console.log("Змінено:", event.target.name);
}
destroy() {
this.form.removeEventListener("submit", this);
this.form.removeEventListener("input", this);
}
}Нижче наведено повний приклад, у якому поєднано:
делегування подій;
клас;
збереження екземпляра в this;
використання currentTarget;
коректне видалення обробника.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Контекст DOM-подій</title>
<style>
.completed {
text-decoration: line-through;
opacity: 0.6;
}
</style>
</head>
<body>
<h1>Список завдань</h1>
<form id="task-form">
<input name="title" required placeholder="Нове завдання">
<button type="submit">Додати</button>
</form>
<ul id="task-list">
<li>
Вивчити this
<button type="button" data-action="complete">Готово</button>
<button type="button" data-action="remove">Видалити</button>
</li>
</ul>
<script>
class TaskList {
constructor(list, form) {
this.list = list;
this.form = form;
this.handleListClick = this.handleListClick.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.list.addEventListener("click", this.handleListClick);
this.form.addEventListener("submit", this.handleSubmit);
}
handleListClick(event) {
const button = event.target.closest("[data-action]");
if (!button || !this.list.contains(button)) {
return;
}
const item = button.closest("li");
const action = button.dataset.action;
if (action === "complete") {
item.classList.toggle("completed");
}
if (action === "remove") {
item.remove();
}
}
handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const input = form.elements.title;
const title = input.value.trim();
if (!title) {
return;
}
const item = document.createElement("li");
item.innerHTML = `
${this.escapeHtml(title)}
<button type="button" data-action="complete">Готово</button>
<button type="button" data-action="remove">Видалити</button>
`;
this.list.append(item);
form.reset();
input.focus();
}
escapeHtml(value) {
const element = document.createElement("div");
element.textContent = value;
return element.innerHTML;
}
destroy() {
this.list.removeEventListener("click", this.handleListClick);
this.form.removeEventListener("submit", this.handleSubmit);
}
}
const list = document.querySelector("#task-list");
const form = document.querySelector("#task-form");
new TaskList(list, form);
</script>
</body>
</html>У методі handleSubmit():
this — екземпляр TaskList;
event.currentTarget — форма;
event.target зазвичай також буде формою, але під час складніших сценаріїв краще розрізняти ці властивості;
this.escapeHtml() викликається на екземплярі контролера.
Методи прив’язуються в конструкторі, тому їх можна безпечно передавати браузеру та видаляти пізніше.
Обробники, задані через HTML-атрибути, мають окремі правила:
<button onclick="console.log(this)">Натиснути</button>У такому обробнику this зазвичай посилається на сам елемент.
Властивість-обробник також використовує звичайну функцію:
button.onclick = function (event) {
console.log(this === event.currentTarget); // true
};Проте в сучасному коді краще використовувати addEventListener(), оскільки він:
дає змогу зареєструвати кілька обробників одного типу;
підтримує опції capture, once, passive, signal;
відокремлює JavaScript від HTML;
зручніший для життєвого циклу компонентів.
addEventListener() і контекстОпції не змінюють правило this, але впливають на життєвий цикл обробника.
const controller = new AbortController();
button.addEventListener(
"click",
function (event) {
console.log(this === event.currentTarget); // true
},
{
signal: controller.signal,
once: true
}
);
// Усі обробники, пов'язані з цим сигналом, буде видалено
controller.abort();Після abort() обробник більше не викликається. Це зручно для компонентів, які потрібно повністю знищувати.
thisПомилково:
button.addEventListener("click", (event) => {
this.disabled = true;
});Правильно:
button.addEventListener("click", (event) => {
event.currentTarget.disabled = true;
});Або:
button.addEventListener("click", function () {
this.disabled = true;
});this замість event.target у делегуванніПомилково:
list.addEventListener("click", function () {
this.classList.add("selected");
});Тут буде вибрано list, а не натиснутий дочірній елемент.
Правильно:
list.addEventListener("click", function (event) {
const item = event.target.closest("li");
if (item) {
item.classList.add("selected");
}
});Помилково:
button.addEventListener("click", controller.handleClick);Якщо handleClick() очікує, що this буде controller, використайте:
const handler = controller.handleClick.bind(controller);
button.addEventListener("click", handler);Помилково:
button.addEventListener("click", method.bind(object));
button.removeEventListener("click", method.bind(object));Правильно:
const handler = method.bind(object);
button.addEventListener("click", handler);
button.removeEventListener("click", handler);currentTarget після awaitПомилково:
button.addEventListener("click", async function (event) {
await saveData();
event.currentTarget.disabled = true;
});Після асинхронної паузи currentTarget може бути null.
Правильно:
button.addEventListener("click", async function (event) {
const buttonElement = event.currentTarget;
await saveData();
buttonElement.disabled = true;
});У класі this може бути екземпляром контролера, а не DOM-елементом:
class Controller {
handleClick(event) {
console.log(this); // Controller
console.log(event.currentTarget); // DOM-елемент
}
}Це нормальна поведінка. Для властивостей контролера використовуйте this, а для поточного DOM-елемента — event.currentTarget.
Для звичайної функції в addEventListener() this дорівнює event.currentTarget.
Стрілкова функція не має власного this.
У класах використовуйте стрілкові методи або bind(), якщо обробник має працювати з екземпляром.
Зберігайте bound-функцію, якщо її потрібно видалити.
У делегуванні подій відрізняйте event.target від event.currentTarget.
Не покладайтеся на currentTarget після await, setTimeout() або іншої асинхронної операції.
Для довгоживучих компонентів передбачайте метод destroy() або використовуйте AbortController.
Якщо значення this не є очевидним, замініть його на явне використання event.currentTarget або збережену змінну.
this у DOM-обробнику визначається механізмом виклику:
звичайна функція через addEventListener() отримує елемент як this;
стрілкова функція успадковує this із зовнішнього контексту;
event.currentTarget вказує на елемент поточного обробника;
event.target вказує на початковий елемент події;
bind() фіксує потрібний контекст, але створює нову функцію;
для видалення обробника потрібно зберігати точне посилання на функцію;
після завершення обробника event.currentTarget може стати недоступним;
у класах потрібно окремо розрізняти this екземпляра та DOM-елемент через event.currentTarget.