Пошук уроків, статей та іншого контенту
Застосуєте правила визначення контексту для налагодження складних викликів і вибору безпечного способу роботи з this.
this складнийthis — це не посилання на функцію і не змінна, яка визначається під час оголошення звичайної функції. Для звичайної функції значення this визначається способом її виклику.
function showThis() {
return this;
}Одна й та сама функція може отримати різний контекст:
const firstObject = { showThis };
const secondObject = { showThis };
console.log(firstObject.showThis() === firstObject); // true
console.log(secondObject.showThis() === secondObject); // trueУ стрілкових функцій правило інше: вони не мають власного this, а захоплюють його з лексичного оточення під час створення.
thisДля звичайного виклику функції корисно аналізувати контекст у такому порядку.
newЯкщо функцію викликають через new, створюється новий об’єкт, і він стає this.
function User(name) {
this.name = name;
}
const user = new User("Олена");
console.log(user.name); // ОленаПід час виклику new User("Олена") відбувається приблизно таке:
створюється новий об’єкт;
його прототипом стає User.prototype;
функція виконується з this, що посилається на цей об’єкт;
якщо функція не повернула інший об’єкт, результатом стає новий об’єкт.
Якщо конструктор явно повертає об’єкт, new поверне саме його:
function Factory() {
this.value = "внутрішній об'єкт";
return {
value: "явно повернений об'єкт"
};
}
const result = new Factory();
console.log(result.value); // явно повернений об'єктПримітивне значення, повернене конструктором, ігнорується.
call, apply або bindМетоди call і apply одразу викликають функцію з указаним this.
function introduce(greeting, punctuation) {
return `${greeting}, я ${this.name}${punctuation}`;
}
const person = { name: "Марко" };
console.log(introduce.call(person, "Привіт", "!"));
// Привіт, я Марко!
console.log(introduce.apply(person, ["Вітаю", "."]));
// Вітаю, я Марко.bind не викликає функцію одразу. Він створює нову функцію з прив’язаним контекстом:
const boundIntroduce = introduce.bind(person, "Добрий день");
console.log(boundIntroduce("?"));
// Добрий день, я Марко?Після bind звичайний виклик уже не змінить прив’язаний контекст:
const anotherPerson = { name: "Софія" };
console.log(boundIntroduce.call(anotherPerson, "!"));
// Добрий день, я Марко!Якщо функцію викликають у формі object.method(), її this — це об’єкт перед крапкою.
const counter = {
value: 0,
increment() {
this.value += 1;
}
};
counter.increment();
console.log(counter.value); // 1Важливий саме синтаксис виклику, а не місце, де функцію було оголошено:
const increment = counter.increment;
increment();У цьому виклику немає об’єкта перед крапкою. Тому increment більше не викликається як метод counter.
Якщо функція викликається без об’єкта, new, call, apply або bind, застосовується default binding.
У строгому режимі:
"use strict";
function showContext() {
return this;
}
console.log(showContext()); // undefinedБез строгого режиму в браузерному сценарії this зазвичай буде глобальним об’єктом. У Node.js результат залежить від способу запуску коду та типу модуля, тому покладатися на таку поведінку не варто.
Безпечніше явно вказувати потрібний контекст або використовувати стрілкову функцію там, де потрібне лексичне захоплення.
thisСтрілкова функція не створює власного this.
"use strict";
const object = {
value: 42,
regularMethod() {
const regularFunction = function () {
return this;
};
const arrowFunction = () => this;
return {
regularFunction,
arrowFunction
};
}
};
const functions = object.regularMethod();
console.log(functions.regularFunction()); // undefined
console.log(functions.arrowFunction() === object); // truearrowFunction захопила this методу regularMethod. Подальший спосіб її виклику не змінює цей контекст:
const detachedArrow = functions.arrowFunction;
console.log(detachedArrow() === object); // true
console.log(detachedArrow.call({ value: 100 }) === object); // truecall, apply і bind не можуть змінити this стрілкової функції.
Стрілка майже ніколи не підходить для методу об’єкта, якщо метод має використовувати this цього об’єкта:
const user = {
name: "Ірина",
regularMethod() {
return this.name;
},
arrowMethod: () => this.name
};
console.log(user.regularMethod()); // Ірина
console.log(user.arrowMethod()); // undefined або значення глобального контекстуarrowMethod створюється не під час виклику через user, тому вона не отримує user як this.
"use strict";
const team = {
name: "Frontend",
getName(prefix = "") {
return `${prefix}${this.name}`;
}
};
const detached = team.getName;
const explicitlyCalled = detached.call({ name: "Backend" });
const bound = team.getName.bind({ name: "QA" });
const boundCalledAsMethod = {
getName: bound
}.getName("Команда: ");
const factory = {
name: "Factory",
create() {
return {
name: "Created",
getName: team.getName
};
}
};
const created = factory.create();
console.log(team.getName("Команда: "));
// Команда: Frontend
console.log(explicitlyCalled);
// Backend
console.log(boundCalledAsMethod);
// Команда: QA
console.log(created.getName("Об'єкт: "));
// Об'єкт: CreatedРозбір:
team.getName(...) — метод викликається через team, тому this === team;
detached.call(...) — call явно встановлює контекст;
bound завжди використовує об’єкт із name: "QA";
created.getName(...) — функція взята з іншого об’єкта, але під час виклику контекстом стає created.
Місце оголошення методу не визначає this. Визначальним є спосіб виклику.
Розглядаючи складний вираз, спочатку знайдіть зовнішню форму виклику.
new має найвищий пріоритетfunction Person(name) {
this.name = name;
}
const context = { name: "Контекст" };
const BoundPerson = Person.bind(context);
const person = new BoundPerson("Андрій");
console.log(person.name); // Андрій
console.log(context.name); // Контекст
console.log(person !== context); // truebind зберігає аргументи, але під час new не змушує конструктор використовувати прив’язаний об’єкт як this.
call і apply явно змінюють контекст звичайної функціїfunction getValue() {
return this.value;
}
const first = { value: 10 };
const second = { value: 20 };
console.log(getValue.call(first)); // 10
console.log(getValue.apply(second)); // 20const object = {
value: 7,
method() {
return this.value;
}
};
console.log(object.method()); // 7
console.log((object.method)()); // 7У простому випадку дужки не змінюють значення. Але якщо спочатку отримати функцію в окремий вираз, контекст втрачається:
const method = object.method;
console.log(method()); // undefined у strict modeОдна з найпоширеніших причин помилок — передавання методу як callback.
"use strict";
const timer = {
seconds: 0,
tick() {
this.seconds += 1;
console.log(this.seconds);
}
};
setTimeout(timer.tick, 100);Функція tick передана окремо. Планувальник викликає її не як timer.tick(), тому this не посилається на timer.
bind"use strict";
const timer = {
seconds: 0,
tick() {
this.seconds += 1;
console.log(`Минуло секунд: ${this.seconds}`);
}
};
const boundTick = timer.tick.bind(timer);
setTimeout(boundTick, 100);setTimeout(() => {
timer.tick();
}, 100);Стрілкова обгортка не замінює контекст методу автоматично. Вона лише виконує саме вираз timer.tick(), у якому timer явно залишається об’єктом виклику.
У класі можна створити властивість-стрілку, яка захопить екземпляр:
class Timer {
seconds = 0;
tick = () => {
this.seconds += 1;
console.log(`Минуло секунд: ${this.seconds}`);
};
}
const timer = new Timer();
setTimeout(timer.tick, 100);Цей підхід зручний для callback, але кожен екземпляр отримує власну функцію tick. Звичайний метод класу зазвичай розміщується в прототипі й може бути економнішим за пам’яттю.
this у класахМетоди класу працюють із this як звичайні функції, але класові методи виконуються у строгому режимі.
class Account {
constructor(owner, balance) {
this.owner = owner;
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
}
}
const account = new Account("Леся", 100);
account.deposit(50);
console.log(account.balance); // 150Якщо від’єднати метод:
const deposit = account.deposit;
deposit(50);виникне помилка або метод не зможе звернутися до властивостей екземпляра, тому що this не буде екземпляром Account.
Виправлення:
const deposit = account.deposit.bind(account);
deposit(50);
console.log(account.balance); // 200supersuper використовується всередині методів класу для звернення до батьківського прототипу:
class BaseLogger {
log(message) {
return `[BASE] ${message}`;
}
}
class Logger extends BaseLogger {
log(message) {
return super.log(message.toUpperCase());
}
}
const logger = new Logger();
console.log(logger.log("готово"));
// [BASE] ГОТОВОВиклик super.log(...) коректно пов’язаний із поточним this. Не варто виносити такі виклики в довільні функції без чіткого розуміння контексту.
thisПоставте виклик у спрощений вигляд:
object.method();Якщо бачите:
const method = object.method;
method();це вже звичайний виклик без контексту object.
thisfunction debugMethod() {
console.log("this:", this);
console.log("this.constructor:", this?.constructor?.name);
}Оператор optional chaining (?.) допоможе безпечно перевірити undefined або null.
debuggerconst service = {
value: 10,
calculate(multiplier) {
debugger;
return this.value * multiplier;
}
};
console.log(service.calculate(3));Якщо код запускається в середовищі з інструментами розробника, виконання зупиниться на debugger. У панелі налагодження можна перевірити:
значення this;
локальні змінні;
стек викликів;
фактичний рядок, з якого викликано функцію.
У цьому прикладі this і змінна user — різні механізми:
const user = {
name: "Назар",
show() {
const callback = () => {
console.log(this.name);
};
callback();
}
};Стрілка читає this із методу show, а не з об’єкта за власним правилом виклику.
Іноді помилка прихована в повторному використанні функції:
const first = { value: 1 };
const second = { value: 2 };
function read() {
return this.value;
}
const boundToFirst = read.bind(first);
console.log(boundToFirst.call(second)); // 1Після першого bind змінити контекст через call або apply не можна.
thisconst cart = {
items: [],
add(item) {
this.items.push(item);
}
};Такий метод зрозумілий і підтримує поліморфізм: його можна викликати для різних об’єктів із відповідною структурою.
bind, коли передаєте метод назовніclass Form {
constructor() {
this.value = "";
this.handleInput = this.handleInput.bind(this);
}
handleInput(value) {
this.value = value;
}
}Це особливо корисно для обробників подій, callback і API, які викликають функцію пізніше.
class Component {
state = { loaded: false };
load() {
Promise.resolve().then(() => {
this.state.loaded = true;
});
}
}Стрілка гарантує, що this усередині callback буде тим самим, що й у load.
this, якщо достатньо явних аргументівФункція з явними параметрами часто простіша для тестування:
function addItem(cart, item) {
cart.items.push(item);
}
const cart = { items: [] };
addItem(cart, "Книга");Такий код не залежить від прихованого правила виклику функції.
this визначається місцем оголошенняconst first = {
value: 1,
read() {
return this.value;
}
};
const second = {
value: 2,
read: first.read
};
console.log(second.read()); // 2Метод оголошено в first, але викликано через second.
const read = second.read;
console.log(read()); // undefined у strict modeЯкщо метод має зберегти контекст, використовуйте:
const read = second.read.bind(second);const object = {
value: 10,
read: () => this.value
};Стрілка не отримує object як this.
this стрілки через callconst arrow = () => this;
console.log(arrow.call({ value: 1 }) === arrow());
// truecall, apply і bind не змінюють лексичний this стрілкової функції.
button.addEventListener("click", controller.handleClick);Якщо API викликає callback зі своїм контекстом або без нього, controller може бути втрачений. Надійніші варіанти:
button.addEventListener("click", controller.handleClick.bind(controller));або:
button.addEventListener("click", () => {
controller.handleClick();
});bindПрив’язування кожного виклику:
items.map(item => process.bind(context)(item));створює зайві функції. Краще створити прив’язану функцію один раз:
const boundProcess = process.bind(context);
items.map(boundProcess);Коли значення this несподіване, дійте послідовно:
Визначте, чи є функція стрілковою.
Перевірте, чи викликається вона через new.
Перевірте наявність call, apply або bind.
Подивіться, чи є виклик формату object.method().
Якщо ні — це звичайний виклик, і в strict mode this буде undefined.
Перевірте, чи не було функцію передано як callback.
Перевірте, чи не було контекст прив’язано раніше через bind.
Дослідіть стек викликів у налагоджувачі.
Вирішіть, чи справді тут потрібен this, або краще передати залежність явним параметром.
У звичайної функції this визначається способом виклику.
Виклик через new створює новий контекст екземпляра.
call і apply одразу задають this.
bind створює нову функцію зі сталим контекстом.
У виклику object.method() значення this — це object.
У звичайному виклику в strict mode this дорівнює undefined.
Стрілкові функції захоплюють this лексично й не реагують на call, apply або bind.
Передавання методу як callback часто призводить до втрати контексту.
Для налагодження потрібно аналізувати форму виклику, стек і фактичне значення this.
Якщо контекст робить код незрозумілим, варто замінити неявний this на явні параметри.