Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Втрата контексту this». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`this` у JavaScript визначається способом виклику, а не місцем оголошення. Щойно метод передають «у відрив» від об'єкта (`setTimeout(user.sayHi)`), він викликається як звичайна функція, і `this` стає `undefined` у strict mode або глобальним об'єктом у нестрогому. Контекст фіксують так: `bind(user)` створює функцію з назавжди прив'язаним `this`; стрілкова функція лексично успадковує `this` із зовнішньої області; анонімна обгортка `() => user.sayHi()` викликає метод через об'єкт; `call` / `apply` задають контекст разово; у класах допомагає `bind` у конструкторі або поле класу зі стрілкою.** ```javascript setTimeout(user.sayHi, 1000); // this is lost setTimeout(user.sayHi.bind(user), 1000); // context is fixed setTimeout(() => user.sayHi(), 1000); // called through the object ``` **Ключове:** `this` прив'язується в момент виклику, тому або фіксуйте його заздалегідь (`bind`, поле класу зі стрілкою), або завжди викликайте метод через об'єкт.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Втрата контексту, це ситуація, коли метод об'єкта викликають не через сам об'єкт, і `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` замість значення.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.