this у JavaScript — чому воно постійно не те
Кілька однакових ключів на столі з різнокольоровими бирками — один ключ, різні двері
Ось код, який зламав більше вечорів, ніж будь-який інший у JavaScript:
const user = {
name: 'Оля',
greet() {
console.log(`Привіт, я ${this.name}`);
},
};
user.greet(); // Привіт, я Оля
setTimeout(user.greet, 100); // Привіт, я undefined
Та сама функція. Той самий об'єкт. Різний результат. Люди у цей момент зазвичай вирішують, що this — це магія, яку треба просто запам'ятати.
Це не магія. Це одне правило, яке ніхто не каже вголос.
Правило, яке пояснює все
this визначається не тим, де функція написана, а тим, як її викликали.
У більшості мов this — це «об'єкт, якому належить метод». У JavaScript функція нікому не належить. Її можна скопіювати у змінну, передати як аргумент, викликати з іншого об'єкта — і this щоразу буде інший. Функція — це не метод об'єкта, а просто значення, яке тимчасово лежить у його властивості.
Аналогія: this — це не ім'я на дверях кабінету, а бейдж «ви зараз розмовляєте з...». Хто зателефонував — той і на бейджі.
Є рівно чотири способи викликати функцію, і кожен дає this за своїм правилом.
Чотири правила прив'язки
1. Виклик через крапку — this дорівнює тому, що ліворуч від крапки
user.greet(); // this === user
Найпростіший випадок і той, який усі інтуїтивно очікують. Що стоїть перед крапкою — те й this. Не «де оголошена функція», а саме що перед крапкою в момент виклику:
const admin = { name: 'Адмін', greet: user.greet };
admin.greet(); // Привіт, я Адмін — та сама функція, інший this
2. Звичайний виклик — this дорівнює undefined
const fn = user.greet;
fn(); // this === undefined → помилка при читанні this.name
Ліворуч від дужок нічого немає — і this нема на що вказувати. У суворому режимі ('use strict', а модулі та класи — завжди суворі) це undefined. У старому несуворому — глобальний об'єкт window, що ще гірше, бо помилка не падає, а тихо псує дані.
Саме це сталося з setTimeout. Ми передали функцію як значення. setTimeout десь усередині викликає її як fn() — без крапки, без об'єкта. Це правило номер два.
3. Явна прив'язка — call, apply, bind
Іноді треба сказати функції прямо: «this — ось цей об'єкт».
user.greet.call(admin); // Привіт, я Адмін — викликати з this = admin
user.greet.apply(admin); // те саме, аргументи передаються масивом
const bound = user.greet.bind(user);
setTimeout(bound, 100); // Привіт, я Оля — this зафіксований назавжди
call і apply викликають одразу. bind не викликає — він повертає нову функцію, у якої this приклеєний назавжди і вже не залежить від способу виклику. Це класичне лікування для setTimeout і обробників подій.
4. Виклик через new — this дорівнює новому об'єкту
function User(name) {
this.name = name; // this — щойно створений порожній об'єкт
}
const u = new User('Оля');
new створює порожній об'єкт, робить його this, виконує функцію і повертає його. Так само працюють класи — детальніше в уроці про прототипи та класи.
Пріоритет, якщо правила конфліктують: new сильніший за bind, bind сильніший за крапку, крапка сильніша за звичайний виклик.
Стрілочні функції — п'яте правило, яке скасовує чотири попередні
Стрілочна функція не має власного this. Узагалі. Вона бере this із того місця, де написана — як звичайну змінну з зовнішньої області видимості.
const user = {
name: 'Оля',
greetLater() {
setTimeout(() => {
console.log(`Привіт, я ${this.name}`); // this з greetLater → user
}, 100);
},
};
user.greetLater(); // Привіт, я Оля
Тут this усередині стрілки — це this методу greetLater, а він викликаний через крапку, тому дорівнює user. Ніяких bind.
Зворотний бік: стрілка як метод об'єкта — майже завжди помилка.
const user = {
name: 'Оля',
greet: () => console.log(this.name), // this тут — НЕ user, а те, що зовні
};
user.greet(); // undefined
Літерал об'єкта не створює області видимості для this, тож стрілка бере його ззовні — з модуля чи глобального коду, де this порожній.
Правило великого пальця: методи об'єктів — звичайні функції; колбеки всередині методів — стрілки.
Три місця, де this губиться найчастіше
Обробники подій. addEventListener викликає функцію з this, рівним елементу, на якому висить обробник — не твоєму об'єкту:
button.addEventListener('click', user.greet); // this === button
button.addEventListener('click', () => user.greet()); // this === user ✓
Методи масивів. forEach, map та інші викликають колбек звичайним викликом — правило номер два:
items.forEach(function (item) { this.process(item); }); // this === undefined
items.forEach((item) => this.process(item)); // стрілка ✓
Методи класу, передані далі. Найпідступніше, бо виглядає як метод:
class Counter {
count = 0;
increment() { this.count++; }
}
const c = new Counter();
button.addEventListener('click', c.increment); // this === button, count не росте
Лікування — або c.increment.bind(c), або оголосити метод як поле-стрілку: increment = () => { this.count++; }. Другий варіант часто зустрінеш у React-коді старого зразка — тепер ти знаєш, навіщо він там.
Шпаргалка
| Як викликано | Чому дорівнює this |
|---|---|
obj.fn() | obj — те, що ліворуч від крапки |
fn() | undefined (у суворому режимі) |
fn.call(x), fn.apply(x) | x |
fn.bind(x) | нова функція з this = x назавжди |
new Fn() | новий порожній об'єкт |
| стрілочна функція | this того місця, де вона написана |
Коли наступного разу побачиш undefined там, де чекав об'єкт, — не шукай помилку в об'єкті. Подивись на дужки: що стояло ліворуч від них у момент виклику? Відповідь майже завжди там.
Якщо об'єкти й методи ще не сидять міцно — урок про об'єкти у курсі якраз про це, а шпаргалка з JavaScript допоможе тримати синтаксис під рукою.