15 березня 2025 р.

Прив’язка контексту до функції

Передаючи методи об’єкту в якості колбеків, наприклад в setTimeout, існує відома проблема: “втрата this”.

В цьому розділі ми розглянемо способи як це можливо виправити.

Втрата “this”

Ми вже розглядали приклади втрати this. Якщо метод передати окремо від об’єкта – this втрачається.

В прикладі наведено як це відбувається з setTimeout:

let user = {
  firstName: "Іван",
  sayHi() {
    alert(`Привіт, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Привіт, undefined!

Як ми бачимо, в модальному вікні браузера відображається не "Іван" як this.firstName, а undefined!

Це тому що setTimeout отримав функцію user.sayHi окремо від об’єкта. Останній рядок може бути переписаний наступним чином:

let f = user.sayHi;
setTimeout(f, 1000); // втрата контексту об’єкта user

Метод setTimeout в браузері трохи особливий: він встановлює this=window під час виклику функції (в Node.js this стає об’єкт таймеру, але це все одно не те, що нам потрібно). Таким чином для this.firstName метод намагається отримати window.firstName, якого не існує. В інших схожих випадках, зазвичай this просто стає undefined.

Задача досить типова – ми хочемо передати метод об’єкту деінде (в цьому випадку – в планувальник) де він буде викликаний. Як бути впевненими в тому, що цей метод об’єкта буде викликаний з правильним контекстом?

Рішення 1: обгортка

Найпростіше рішення – використати функцію обгортку:

let user = {
  firstName: "Іван",
  sayHi() {
    alert(`Привіт, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Привіт, Іван!
}, 1000);

Тепер це працює, бо ми отримали user з зовнішнього лексичного оточення, і потім викликали метод.

Аналогічний запис, тільки коротший:

setTimeout(() => user.sayHi(), 1000); // Привіт, Іван!

Виглядає чудово, але з’являється легка вразливість в структурі нашого коду.

Що якщо перед спрацюванням setTimeout (з однією секундою затримки!) змінна user змінить своє значення? Тоді, неочікувано, наша обгортка викличе неправильний об’єкт!

let user = {
  firstName: "Іван",
  sayHi() {
    alert(`Привіт, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ...значення user змінюється впродовж 1 секунди
user = {
  sayHi() { alert("Інший user в setTimeout!"); }
};

// Інший user в setTimeout!

Наступне рішення гарантує, що така ситуація не трапиться.

Рішення 2: прив’язка (bind)

Функції надають нам вбудований метод