Втрата контексту this
Втрата контексту, це ситуація, коли метод об'єкта викликають не через сам об'єкт, і this усередині нього перестає вказувати на цей об'єкт. У JavaScript this не «зашите» у функцію під час оголошення: воно обчислюється щоразу заново, залежно від того, як саме функцію викликали.
Теорія
TL;DR
thisвизначає спосіб виклику: важливо, що стоїть ліворуч від крапки, а не де функцію оголошено.- Передача методу як посилання (
setTimeout(user.sayHi)) розриває зв'язок з об'єктом. bind(obj)повертає нову функцію з назавжди зафіксованим контекстом.- Стрілкові функції не мають власного
thisі беруть його лексично із зовнішньої області. call/applyзадають контекст тимчасово, лише на час одного виклику.- У класах контекст фіксують через
bindу конструкторі або через поле класу зі стрілковою функцією.
Швидкий приклад
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() створює нову функцію з жорстко прив'язаним контекстом, незалежно від того, як її потім викличуть:
setTimeout(user.sayHi.bind(user), 1000); // "Hi, I am Oleh"call і apply, навпаки, не створюють нову функцію, а виконують наявну із заданим контекстом просто зараз. Це тимчасова прив'язка, яка діє лише під час цього виклику:
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: вона бере його з тієї області, де її створено. Тому всередині методу вона «замикає» правильний контекст:
const user = {
name: 'Oleh',
sayHiLater() {
setTimeout(() => {
console.log(`Hi, I am ${this.name}`);
}, 1000);
}
};
user.sayHiLater(); // "Hi, I am Oleh"До появи стрілкових функцій той самий ефект давало збереження контексту у змінну self або that:
const user = {
name: 'Oleh',
sayHiLater() {
const self = this;
setTimeout(function() {
console.log(`Hi, I am ${self.name}`);
}, 1000);
}
};
user.sayHiLater(); // "Hi, I am Oleh"Найпростіший варіант, коли ви самі контролюєте місце передавання, це взагалі не передавати метод напряму, а викликати його всередині анонімної обгортки:
setTimeout(() => user.sayHi(), 1000); // "Hi, I am Oleh"Тут user.sayHi() викликається через об'єкт, тож контекст коректний.
Контекст у класах
У класі this зазвичай не губиться, поки метод викликають через екземпляр:
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):
const fn = oleh.sayHi;
fn(); // TypeError: Cannot read properties of undefinedКласичне рішення, прив'язка в конструкторі:
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 екземпляра:
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замість значення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.