Пошук уроків, статей та іншого контенту
З’ясуєте, як втрачається контекст під час передавання методу як окремої функції, і розглянете типові помилки.
У JavaScript this — це значення, яке визначається способом виклику функції, а не місцем її оголошення.
Одна й та сама функція може отримати різний контекст:
"use strict";
function showName() {
console.log(this.name);
}
const firstUser = { name: "Олена", showName };
const secondUser = { name: "Андрій", showName };
firstUser.showName(); // Олена
secondUser.showName(); // АндрійУ цих прикладах функція showName викликається як метод об’єкта:
firstUser.showName();Тому this посилається на firstUser.
Контекстом називають значення this, доступне всередині функції під час її виконання.
Проблема виникає, коли метод передають як окрему функцію:
"use strict";
const user = {
name: "Олена",
showName() {
console.log(this.name);
}
};
user.showName(); // Олена
const showName = user.showName;
showName(); // TypeError або помилка доступу до undefinedПід час виклику user.showName() об’єкт перед крапкою визначає значення this.
А під час виклику showName() перед крапкою вже немає об’єкта. Метод перетворився на звичайний виклик функції, тому його початковий контекст не зберігся.
Це можна уявити так:
user.showName(); // this === user
showName(); // this не прив’язаний до userСаме передавання методу як окремого значення часто називають втратою контексту.
Одна з найпоширеніших ситуацій — передавання методу в callback.
"use strict";
const user = {
name: "Олена",
showName() {
console.log(this.name);
}
};
setTimeout(user.showName, 100);setTimeout отримує функцію, але не знає, що вона спочатку була методом user. Через деякий час він викликає її як звичайну функцію.
Схожа проблема виникає з методами масивів:
"use strict";
const users = [
{ name: "Олена" },
{ name: "Андрій" }
];
const printer = {
prefix: "Користувач:",
print(user) {
console.log(this.prefix, user.name);
}
};
users.forEach(printer.print);
// Контекст printer втраченоМетод printer.print передається в forEach окремо. Під час виклику forEach значення this всередині print не буде автоматично встановлено як printer.
Найпростіший спосіб зберегти контекст — викликати метод усередині стрілкової функції:
"use strict";
const users = [
{ name: "Олена" },
{ name: "Андрій" }
];
const printer = {
prefix: "Користувач:",
print(user) {
console.log(this.prefix, user.name);
}
};
users.forEach((user) => {
printer.print(user);
});Тут printer.print не передається безпосередньо. Замість цього forEach отримує стрілкову функцію, яка під час виконання явно викликає метод через printer.
Стрілкова функція не створює власного this, але в цьому прикладі важливим є саме явне звернення до printer.
Такий підхід зручний, коли callback потребує додаткових аргументів або логіки:
users.forEach((user, index) => {
console.log(`${index + 1}.`);
printer.print(user);
});bindМетод bind створює нову функцію, назавжди прив’язану до вказаного об’єкта:
"use strict";
const user = {
name: "Олена",
showName() {
console.log(this.name);
}
};
const boundShowName = user.showName.bind(user);
setTimeout(boundShowName, 100);Під час виклику boundShowName значення this буде user, навіть якщо функцію викликає інший код.
bind не викликає функцію одразу. Він повертає нову функцію.
const boundShowName = user.showName.bind(user);
boundShowName(); // Функція викликається тутЦе відрізняється від такого коду:
user.showName.bind(user)();Тут функція спочатку прив’язується, а потім одразу викликається.
bindbind може прив’язати не лише this, а й початкові аргументи:
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: "Олена" };
const greetUser = greet.bind(user, "Привіт");
greetUser("!");
// Привіт, Олена!Перший аргумент bind — це значення this, а наступні аргументи будуть передані функції заздалегідь.
call або applyІноді контекст потрібно встановити лише під час конкретного виклику.
callМетод call викликає функцію одразу й передає аргументи окремо:
function showMessage(message) {
console.log(this.name, message);
}
const user = { name: "Олена" };
showMessage.call(user, "Вітаю!");
// Олена Вітаю!applyapply працює подібно до call, але приймає аргументи масивом:
function sum(first, second) {
return this.base + first + second;
}
const context = { base: 10 };
const result = sum.apply(context, [5, 7]);
console.log(result); // 22У сучасному JavaScript call та apply використовують рідше, оскільки для багатьох задач достатньо bind або стрілкової функції.
Методи класу також можуть втрачати контекст.
"use strict";
class Counter {
constructor() {
this.value = 0;
}
increment() {
this.value += 1;
console.log(this.value);
}
}
const counter = new Counter();
setTimeout(counter.increment, 100);Метод increment переданий окремо, тому під час виконання this не буде автоматично посилатися на counter.
Один зі способів — прив’язати метод у конструкторі:
"use strict";
class Counter {
constructor() {
this.value = 0;
this.increment = this.increment.bind(this);
}
increment() {
this.value += 1;
console.log(this.value);
}
}
const counter = new Counter();
setTimeout(counter.increment, 100);Тепер counter.increment — це вже прив’язана функція, яка зберігає правильний контекст.
Цей підхід особливо корисний, коли метод передається в різні callback-и або обробники подій.
У сучасному JavaScript можна оголосити метод як стрілкове поле класу:
"use strict";
class Counter {
value = 0;
increment = () => {
this.value += 1;
console.log(this.value);
};
}
const counter = new Counter();
setTimeout(counter.increment, 100);Стрілкова функція захоплює this екземпляра під час його створення. Тому передавання counter.increment не втрачає контекст.
Варто пам’ятати, що це не звичайний метод прототипу. Кожен екземпляр класу отримує власну функцію increment.
Стрілкові функції не мають власного this. Вони використовують this із зовнішньої області видимості:
"use strict";
const user = {
name: "Олена",
regularMethod() {
console.log(this.name);
},
arrowMethod: () => {
console.log(this.name);
}
};
user.regularMethod(); // Олена
user.arrowMethod(); // undefinedСтрілкова функція arrowMethod не отримує this від об’єкта user. Вона бере його із зовнішнього контексту, де об’єкт user не є значенням this.
Тому стрілкові функції зазвичай не використовують як методи об’єктів, якщо метод має працювати з властивостями через this.
Натомість стрілкові функції добре підходять для callback-ів:
const user = {
name: "Олена",
showName() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
user.showName();Внутрішня стрілкова функція захоплює this методу showName. У результаті вона може звернутися до user.
this у різних типах викликуconst user = {
name: "Олена",
showName() {
console.log(this.name);
}
};
user.showName(); // this === userУ суворому режимі this у звичайній функції дорівнює undefined:
"use strict";
function showContext() {
console.log(this);
}
showContext(); // undefinedБез суворого режиму в браузері значення може бути глобальним об’єктом. Не варто покладатися на таку поведінку: вона ускладнює пошук помилок.
newПід час виклику функції-конструктора через new створюється новий об’єкт, і this посилається на нього:
function User(name) {
this.name = name;
}
const user = new User("Олена");
console.log(user.name); // Оленаcall, apply або bindУ цих випадках контекст задається явно:
function showName() {
console.log(this.name);
}
const user = { name: "Олена" };
showName.call(user); // ОленаРозглянемо об’єкт, який реєструє повідомлення через таймер:
"use strict";
class Logger {
constructor(prefix) {
this.prefix = prefix;
this.log = this.log.bind(this);
}
log(message) {
console.log(`[${this.prefix}] ${message}`);
}
start() {
setTimeout(this.log, 100, "Операцію завершено");
}
}
const logger = new Logger("INFO");
logger.start();Метод log передається в setTimeout окремо, але прив’язування в конструкторі гарантує, що this залишиться екземпляром Logger.
У Node.js і браузерах setTimeout може передавати callback-у додаткові аргументи після затримки:
setTimeout(this.log, 100, "Операцію завершено");Однак контекст і аргументи — це різні речі:
bind(this) зберігає значення this;
"Операцію завершено" є аргументом callback-у.
button.addEventListener("click", () => {
controller.handleClick();
});Цей підхід прозорий і зручний для коротких callback-ів.
bind, якщо одну функцію передають багато разівthis.handleClick = this.handleClick.bind(this);
button.addEventListener("click", this.handleClick);Це також дає змогу пізніше передати ту саму функцію для видалення обробника:
button.removeEventListener("click", this.handleClick);Якщо замість цього щоразу створювати нову стрілкову функцію, посилання будуть різними:
button.addEventListener("click", () => this.handleClick());
button.removeEventListener("click", () => this.handleClick());
// Обробник не буде видаленоclass FormController {
submit = (event) => {
event.preventDefault();
console.log("Форму надіслано");
};
}Це зручно для обробників подій, але потрібно враховувати створення окремої функції для кожного екземпляра класу.
const user = {
name: "Олена",
showName() {
console.log(this.name);
}
};
const callback = user.showName;
callback();Функція не зберігає об’єкт, з якого її отримали. Якщо це потрібно, використовуйте bind або обгортку.
bind одразу замість передавання функціїНеправильно:
setTimeout(user.showName.bind(user)(), 100);Тут showName викликається одразу, а результат її виконання передається в setTimeout.
Правильно:
setTimeout(user.showName.bind(user), 100);Неправильно:
button.addEventListener("click", controller.handleClick.bind(controller));
button.removeEventListener("click", controller.handleClick.bind(controller));Кожен виклик bind створює нову функцію. Тому функція, передана в removeEventListener, не збігається з тією, яку було зареєстровано.
Правильно:
controller.handleClick = controller.handleClick.bind(controller);
button.addEventListener("click", controller.handleClick);
button.removeEventListener("click", controller.handleClick);const user = {
name: "Олена",
showName: () => {
console.log(this.name);
}
};Така функція не отримає user як this. Для методу краще використовувати звичайний синтаксис:
const user = {
name: "Олена",
showName() {
console.log(this.name);
}
};this у стрілкової функціїconst showName = () => {
console.log(this.name);
};
showName.call({ name: "Олена" });call, apply і bind не змінюють this стрілкової функції. Її this визначається зовнішнім контекстом.
Не кожен API викликає callback так, як очікується. Передавання методу:
items.map(object.transform);не означає, що object.transform отримає object як this. Якщо метод залежить від контексту, його потрібно явно зберегти:
items.map((item) => object.transform(item));або:
items.map(object.transform.bind(object));Якщо метод працює правильно під час прямого виклику, але помиляється як callback, перевірте:
Чи викликається він через об’єкт:
object.method();Чи не було його спочатку збережено в змінну:
const method = object.method;Чи не передано його в setTimeout, forEach, обробник події або інший API без прив’язування.
Чи не використано стрілкову функцію там, де потрібен динамічний this.
Чи зберігається посилання на функцію, створену через bind.
Для перевірки можна тимчасово вивести this у консоль:
"use strict";
const service = {
run() {
console.log("Контекст:", this);
}
};
service.run();
const run = service.run;
run();this визначається способом виклику функції.
У виклику object.method() this посилається на object.
Під час передавання object.method як окремої функції цей контекст втрачається.
Для збереження контексту використовуйте:
обгортку зі стрілковою функцією;
bind;
стрілкове поле класу;
явний виклик через call або apply.
Стрілкові функції не мають власного this і не підходять як методи, яким потрібен контекст об’єкта.
Для removeEventListener та подібних API потрібно зберігати посилання на ту саму функцію, яку було передано під час реєстрації.