Пошук уроків, статей та іншого контенту
Поясните, як this визначається під час виклику методу, і навчитеся безпечно працювати з контекстом об'єкта.
thisУ JavaScript значення this у звичайній функції визначається переважно під час виклику, а не під час її створення.
Це особливо важливо для методів прототипу. Функція може бути оголошена один раз, але викликана з різними об’єктами, тому в різних викликах this може посилатися на різні значення.
"use strict";
function greet() {
return `Привіт, я ${this.name}`;
}
const firstUser = {
name: "Олена",
greet
};
const secondUser = {
name: "Андрій",
greet
};
console.log(firstUser.greet()); // Привіт, я Олена
console.log(secondUser.greet()); // Привіт, я АндрійУ цьому прикладі функція greet одна, але:
у виклику firstUser.greet() значення this — firstUser;
у виклику secondUser.greet() значення this — secondUser.
Важливий сам спосіб виклику:
object.method();У такому виклику об’єкт перед крапкою стає значенням this.
Прототипний метод — це метод, доданий до prototype конструктора або класу. Усі екземпляри можуть використовувати такий метод, не створюючи окрему копію функції для кожного об’єкта.
"use strict";
function User(name) {
this.name = name;
}
User.prototype.sayHello = function () {
return `Привіт, ${this.name}!`;
};
const user = new User("Марія");
console.log(user.sayHello()); // Привіт, Марія!Під час виклику user.sayHello():
JavaScript шукає sayHello спочатку безпосередньо в user;
якщо не знаходить, шукає метод у прототипі User.prototype;
викликає функцію з this, що дорівнює user.
Саме тому метод, розташований у прототипі, може працювати з властивостями конкретного екземпляра.
"use strict";
function Counter(initialValue = 0) {
this.value = initialValue;
}
Counter.prototype.increment = function () {
this.value += 1;
};
Counter.prototype.getValue = function () {
return this.value;
};
const firstCounter = new Counter(10);
const secondCounter = new Counter(100);
firstCounter.increment();
firstCounter.increment();
secondCounter.increment();
console.log(firstCounter.getValue()); // 12
console.log(secondCounter.getValue()); // 101
console.log(firstCounter.increment === secondCounter.increment); // trueОбидва лічильники використовують одну функцію increment, але this у кожному виклику посилається на відповідний об’єкт.
thisДля звичайних функцій корисно аналізувати виклик у такому порядку.
const user = {
name: "Ірина",
getName() {
return this.name;
}
};
console.log(user.getName()); // ІринаУ виразі user.getName() значення this — user.
Якщо метод викликається через вкладений об’єкт, this буде об’єктом, який стоїть безпосередньо перед крапкою:
const company = {
name: "Tech Company",
employee: {
name: "Петро",
getName() {
return this.name;
}
}
};
console.log(company.employee.getName()); // ПетроТут this — company.employee, а не company.
Якщо функцію викликати без об’єкта перед нею, this залежить від режиму виконання.
У строгому режимі this дорівнює undefined:
"use strict";
function showContext() {
console.log(this);
}
showContext(); // undefinedБез строгого режиму в браузерному скрипті старого стилю this у такому випадку може посилатися на глобальний об’єкт. Не варто покладатися на таку поведінку. Для передбачуваного коду використовуйте строгий режим або сучасні модулі, де код виконується у строгому режимі.
call, apply або bindЦі методи дають змогу явно керувати значенням this.
"use strict";
function introduce(role) {
return `${this.name} — ${role}`;
}
const user = {
name: "Олег"
};
console.log(introduce.call(user, "розробник"));
// Олег — розробник
console.log(introduce.apply(user, ["тестувальник"]));
// Олег — тестувальник
const introduceUser = introduce.bind(user);
console.log(introduceUser("менеджер"));
// Олег — менеджерРізниця:
call(context, arg1, arg2) викликає функцію одразу, передаючи аргументи окремо;
apply(context, [arg1, arg2]) викликає функцію одразу, передаючи аргументи масивом;
bind(context, ...) повертає нову функцію із зафіксованим this.
newЯкщо функцію викликають з new, JavaScript створює новий об’єкт і встановлює його як this.
function Product(name, price) {
this.name = name;
this.price = price;
}
const product = new Product("Клавіатура", 2500);
console.log(product.name); // Клавіатура
console.log(product.price); // 2500Крім того, створений об’єкт отримує прототип Product.prototype.
Поширена помилка — дістати метод з об’єкта й викликати його окремо.
"use strict";
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};
const user = new User("Софія");
console.log(user.getName()); // Софія
const getName = user.getName;
console.log(getName()); // Помилка або звернення до undefinedПісля присвоєння:
const getName = user.getName;змінна getName містить лише функцію. Інформація про те, що раніше вона була властивістю user, не зберігається.
Виклик:
getName();не має об’єкта перед крапкою, тому метод втрачає початковий контекст.
bind"use strict";
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};
const user = new User("Софія");
const getName = user.getName.bind(user);
console.log(getName()); // Софіяbind створює нову функцію, для якої this назавжди прив’язаний до user.
Це зручно, коли метод передають як callback:
"use strict";
function Timer(label) {
this.label = label;
}
Timer.prototype.log = function () {
console.log(`Таймер: ${this.label}`);
};
const timer = new Timer("завантаження");
setTimeout(timer.log.bind(timer), 100);Без bind callback був би викликаний окремо від об’єкта, і метод не отримав би очікуваний контекст.
call, apply та bind для прототипних методівМетод прототипу можна викликати для будь-якого сумісного об’єкта, якщо його властивості відповідають очікуванням методу.
"use strict";
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
return `Привіт, мене звати ${this.name}`;
};
const objectWithoutPersonPrototype = {
name: "Дмитро"
};
console.log(Person.prototype.sayHello.call(objectWithoutPersonPrototype));
// Привіт, мене звати ДмитроМетод не перевіряє, чи є objectWithoutPersonPrototype екземпляром Person. Йому достатньо, щоб у переданого об’єкта була властивість name.
Такий підхід називають роботою за принципом «качиного типізування»: важливо не походження об’єкта, а наявність потрібної поведінки або властивостей.
thisСтрілкові функції не мають власного this. Вони використовують this із зовнішнього лексичного контексту.
const user = {
name: "Назар",
getName: () => {
return this.name;
}
};
console.log(user.getName()); // Не "Назар"Стрілкова функція не отримує this від виклику user.getName(). Вона бере this із місця, де була створена. Для методів об’єкта така поведінка зазвичай неправильна.
Звичайна функція підходить для методу:
const user = {
name: "Назар",
getName() {
return this.name;
}
};
console.log(user.getName()); // НазарСтрілкова функція корисна всередині методу, коли потрібно зберегти this зовнішнього методу в callback.
"use strict";
function Team(name, members) {
this.name = name;
this.members = members;
}
Team.prototype.printMembers = function () {
this.members.forEach((member) => {
console.log(`${this.name}: ${member}`);
});
};
const team = new Team("Frontend", ["Аліна", "Віктор"]);
team.printMembers();
// Frontend: Аліна
// Frontend: ВікторСтрілкова функція в forEach не створює власного this, тому використовує this методу printMembers.
Якщо використати звичайну callback-функцію, її контекст потрібно було б обробити окремо:
"use strict";
function Team(name, members) {
this.name = name;
this.members = members;
}
Team.prototype.printMembers = function () {
this.members.forEach(function (member) {
console.log(`${this.name}: ${member}`);
}, this);
};
const team = new Team("Backend", ["Оксана", "Тарас"]);
team.printMembers();
// Backend: Оксана
// Backend: ТарасДругий аргумент forEach задає значення this для звичайної callback-функції.
thisМетоди класів також є звичайними функціями з погляду правил виклику this.
"use strict";
class Account {
constructor(owner, balance) {
this.owner = owner;
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
}
getBalance() {
return `${this.owner}: ${this.balance}`;
}
}
const account = new Account("Катерина", 1000);
account.deposit(500);
console.log(account.getBalance()); // Катерина: 1500Синтаксис класу приховує роботу з прототипом, але deposit і getBalance зберігаються в Account.prototype.
console.log(
Account.prototype.getBalance === account.getBalance
); // trueМетод класу також може втратити контекст:
const printBalance = account.getBalance;
// Виклик без account перед крапкою втрачає контекст.
console.log(printBalance()); // Помилка або некоректний результатБезпечний варіант:
const printBalance = account.getBalance.bind(account);
console.log(printBalance()); // Катерина: 1500Якщо метод очікує this.name, об’єкт повинен мати властивість name. За потреби можна додати перевірку:
"use strict";
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
if (typeof this.name !== "string") {
throw new TypeError("Об'єкт має містити рядкову властивість name");
}
return this.name;
};
const user = new User("Леся");
console.log(user.getName()); // ЛесяТакі перевірки допомагають швидше виявити неправильний виклик методу.
thisМетод повинен працювати з об’єктом, для якого його викликають, а не з глобальними змінними:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};Такий код зрозуміліший і безпечніший, ніж звернення до глобального об’єкта або глобальної змінної name.
Якщо метод передається іншому коду, заздалегідь прив’яжіть контекст:
"use strict";
function Logger(prefix) {
this.prefix = prefix;
}
Logger.prototype.log = function (message) {
console.log(`[${this.prefix}] ${message}`);
};
const logger = new Logger("INFO");
const log = logger.log.bind(logger);
log("Застосунок запущено");
// [INFO] Застосунок запущеноНе потрібно прив’язувати кожен метод без потреби. Робіть це тоді, коли метод справді передається як окрема функція.
const user = {
name: "Анна",
getName() {
return this.name;
}
};
const anotherObject = {
name: "Богдан",
getName: user.getName
};
console.log(anotherObject.getName()); // БогданФункція та сама, але this — anotherObject, бо саме він стоїть перед крапкою під час виклику.
this запам’ятає місце оголошенняconst firstObject = {
name: "Перший",
getName() {
return this.name;
}
};
const secondObject = {
name: "Другий",
getName: firstObject.getName
};
console.log(secondObject.getName()); // ДругийЗвичайна функція не запам’ятовує firstObject. Її this визначається способом конкретного виклику.
function User(name) {
this.name = name;
}
// Такий метод не отримає this від user.getName().
User.prototype.getName = () => {
return this.name;
};Для методів прототипу використовуйте звичайні функції:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};bind"use strict";
function Button(label) {
this.label = label;
}
Button.prototype.handleClick = function () {
console.log(`Натиснуто: ${this.label}`);
};
const button = new Button("Зберегти");
const handler = button.handleClick;
// Контекст button втрачено.
handler();Виправлення:
const handler = button.handleClick.bind(button);
handler(); // Натиснуто: Зберегтиbind із неправильним об’єктомconst firstUser = { name: "Аліса" };
const secondUser = { name: "Максим" };
function getName() {
return this.name;
}
const getFirstUserName = getName.bind(firstUser);
console.log(getFirstUserName()); // Аліса
// Повторно змінити this через call не вийде.
console.log(getFirstUserName.call(secondUser)); // АлісаПісля bind значення this уже зафіксоване. call і apply не можуть його замінити.
Нижче наведено повний приклад конструктора з прототипними методами, передаванням callback і явним зв’язуванням контексту.
"use strict";
function Cart(owner) {
this.owner = owner;
this.items = [];
}
Cart.prototype.addItem = function (name, price) {
this.items.push({ name, price });
};
Cart.prototype.getTotal = function () {
return this.items.reduce((total, item) => total + item.price, 0);
};
Cart.prototype.printSummary = function () {
console.log(`Кошик користувача: ${this.owner}`);
console.log(`Кількість товарів: ${this.items.length}`);
console.log(`Загальна сума: ${this.getTotal()} грн`);
};
const cart = new Cart("Іван");
cart.addItem("Миша", 800);
cart.addItem("Клавіатура", 2200);
cart.printSummary();
const printSummary = cart.printSummary.bind(cart);
printSummary();Результат:
Кошик користувача: Іван
Кількість товарів: 2
Загальна сума: 3000 грн
Кошик користувача: Іван
Кількість товарів: 2
Загальна сума: 3000 грнМетоди addItem, getTotal і printSummary спільні для всіх кошиків, але працюють із даними конкретного екземпляра через this.
Коли потрібно визначити значення this, виконайте такі кроки:
Знайдіть фактичний виклик функції.
Перевірте, чи є перед функцією об’єкт і крапка:
object.method();Якщо функція викликається через call, apply або bind, перевірте переданий контекст.
Якщо використано new, this — новий створений об’єкт.
Якщо це стрілкова функція, знайдіть this у зовнішньому контексті.
Якщо функцію викликають окремо, врахуйте строгий режим: this буде undefined.
У звичайних функціях this визначається способом виклику.
У виклику object.method() значення this — object.
Прототипний метод є спільним для екземплярів, але this посилається на конкретний екземпляр під час виклику.
Якщо метод дістати з об’єкта й викликати окремо, контекст буде втрачено.
bind створює функцію із зафіксованим this.
call і apply викликають функцію одразу з указаним контекстом.
Стрілкові функції не мають власного this, тому зазвичай не підходять для прототипних методів.
Для callback усередині методу стрілкові функції часто зручні, оскільки зберігають this зовнішнього методу.
Щоб безпечно працювати з контекстом, завжди аналізуйте фактичний спосіб виклику функції.