Skip to main content

Втрата контексту this

Втрата контексту, це ситуація, коли метод об'єкта викликають не через сам об'єкт, і this усередині нього перестає вказувати на цей об'єкт. У JavaScript this не «зашите» у функцію під час оголошення: воно обчислюється щоразу заново, залежно від того, як саме функцію викликали.

Теорія

TL;DR

  • this визначає спосіб виклику: важливо, що стоїть ліворуч від крапки, а не де функцію оголошено.
  • Передача методу як посилання (setTimeout(user.sayHi)) розриває зв'язок з об'єктом.
  • bind(obj) повертає нову функцію з назавжди зафіксованим контекстом.
  • Стрілкові функції не мають власного this і беруть його лексично із зовнішньої області.
  • call / apply задають контекст тимчасово, лише на час одного виклику.
  • У класах контекст фіксують через bind у конструкторі або через поле класу зі стрілковою функцією.

Швидкий приклад

javascript
const user = { name: 'Oleh', sayHi() { console.log(`Hi, I am ${this.name}`); } }; user.sayHi(); // "Hi, I am Oleh" setTimeout(user.sayHi, 1000); // "Hi, I am undefined"

Чому this губиться

setTimeout отримує лише посилання на функцію і викликає її як callback(), а не як user.sayHi(). Ліворуч від крапки більше немає об'єкта, тож прив'язка до user зникає:

  • у strict mode (а модулі ES завжди strict) this дорівнює undefined, і звернення this.name кидає TypeError;
  • у нестрогому режимі this підмінюється глобальним об'єктом (window у браузері), і this.name тихо повертає undefined.

Те саме стається з будь-якою передачею методу «у відрив»: обробники подій, array.map(obj.method), збереження методу у змінну.

Прив'язка контексту: bind, call і apply

Function.prototype.bind() створює нову функцію з жорстко прив'язаним контекстом, незалежно від того, як її потім викличуть:

javascript
setTimeout(user.sayHi.bind(user), 1000); // "Hi, I am Oleh"

call і apply, навпаки, не створюють нову функцію, а виконують наявну із заданим контекстом просто зараз. Це тимчасова прив'язка, яка діє лише під час цього виклику:

javascript
function greet() { console.log(`Hi, ${this.name}`); } const user = { name: 'Oleh' }; greet.call(user); // "Hi, Oleh" greet.apply(user); // "Hi, Oleh"

Різниця між ними лише в передаванні аргументів: call приймає їх списком, apply, масивом.

Стрілкові функції, збереження self і обгортка

Стрілкова функція не має власного this: вона бере його з тієї області, де її створено. Тому всередині методу вона «замикає» правильний контекст:

javascript
const user = { name: 'Oleh', sayHiLater() { setTimeout(() => { console.log(`Hi, I am ${this.name}`); }, 1000); } }; user.sayHiLater(); // "Hi, I am Oleh"

До появи стрілкових функцій той самий ефект давало збереження контексту у змінну self або that:

javascript
const user = { name: 'Oleh', sayHiLater() { const self = this; setTimeout(function() { console.log(`Hi, I am ${self.name}`); }, 1000); } }; user.sayHiLater(); // "Hi, I am Oleh"

Найпростіший варіант, коли ви самі контролюєте місце передавання, це взагалі не передавати метод напряму, а викликати його всередині анонімної обгортки:

javascript
setTimeout(() => user.sayHi(), 1000); // "Hi, I am Oleh"

Тут user.sayHi() викликається через об'єкт, тож контекст коректний.

Контекст у класах

У класі this зазвичай не губиться, поки метод викликають через екземпляр:

javascript
class User { constructor(name) { this.name = name; } sayHi() { console.log(`Hi, ${this.name}`); } } const oleh = new User('Oleh'); oleh.sayHi(); // "Hi, Oleh"

Але варто відірвати метод від екземпляра, і контекст знову зникає (тіло класу завжди виконується у strict mode, тож тут буде TypeError):

javascript
const fn = oleh.sayHi; fn(); // TypeError: Cannot read properties of undefined

Класичне рішення, прив'язка в конструкторі:

javascript
class User { constructor(name) { this.name = name; this.sayHi = this.sayHi.bind(this); // фіксуємо контекст назавжди } sayHi() { console.log(`Hi, ${this.name}`); } } const oleh = new User('Oleh'); setTimeout(oleh.sayHi, 1000); // "Hi, Oleh"

Саме так роками писали класові компоненти React: this.handleClick = this.handleClick.bind(this).

Сучасніший варіант, поля класу (class fields, ES2022): метод, оголошений як стрілкова функція, автоматично успадковує this екземпляра:

javascript
class User { name = 'Oleh'; sayHi = () => { console.log(`Hi, ${this.name}`); }; } const oleh = new User(); setTimeout(oleh.sayHi, 1000); // "Hi, Oleh"

Синтаксис полів класу підтримують усі сучасні браузери. Ціна, така функція створюється окремо для кожного екземпляра, а не лежить один раз у прототипі.

Підсумкова таблиця

СпосібЩо робитьДе застосовують
.bind(this)Створює нову функцію з фіксованим контекстомКолбеки, React, таймери
Стрілкова функціяУспадковує this із зовнішньої областіsetTimeout, async, проміси
const self = thisСтарий спосіб зберегти посилання на контекстLegacy-код, колбеки
call / applyТимчасово задає контекст на час викликуОдноразові виклики
Прив'язка в класіbind у конструкторі або стрілка в полі класуReact-класи, ООП
Анонімна обгорткаВикликає метод через об'єктПрості колбеки

Типові помилки

  • Думати, що this залежить від місця оголошення. Воно залежить лише від способу виклику; та сама функція в різних викликах має різний this.
  • Викликати bind у методі рендеру або в обробнику. bind щоразу повертає нову функцію, тому порівняння посилань ламається, а removeEventListener не може зняти підписку. Прив'язуйте один раз, у конструкторі чи полі класу.
  • Намагатися «перепризначити» this стрілковій функції. arrow.call(obj) і arrow.bind(obj) не діють: власного this у стрілки немає.
  • Використовувати стрілку як метод об'єктного літерала. { name: 'Oleh', sayHi: () => this.name } бере this із зовнішньої області, а не з об'єкта.
  • Забувати, що втрата тиха. У нестрогому режимі помилки не буде: ви просто побачите undefined замість значення.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.