Вивчай
· 5 хв читання
javascriptпочатківцям

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 допоможе тримати синтаксис під рукою.