Пошук уроків, статей та іншого контенту
Додаватимете спільні методи до prototype конструктора та порівняєте їх із методами, створеними для кожного об'єкта.
Конструктор дає змогу створювати багато однотипних об’єктів. Якщо додати метод безпосередньо всередину конструктора, кожен створений об’єкт отримає власну копію цього методу.
У більшості випадків це зайве. Спільні методи краще додавати до властивості prototype конструктора. Тоді всі екземпляри зможуть використовувати один і той самий метод.
Розглянемо конструктор User:
function User(name, age) {
this.name = name;
this.age = age;
this.greet = function () {
return `Привіт, мене звати ${this.name}.`;
};
}
const firstUser = new User("Олена", 25);
const secondUser = new User("Андрій", 30);
console.log(firstUser.greet());
console.log(secondUser.greet());
console.log(firstUser.greet === secondUser.greet); // falseПід час кожного виклику new User(...) створюється нова функція greet.
Тобто:
firstUser.greet !== secondUser.greet;Методи працюють правильно, але кожен об’єкт зберігає власну функцію в пам’яті. Якщо об’єктів багато, це може бути неефективно.
prototypeКожна звичайна функція-конструктор має властивість prototype. Це об’єкт, який використовується як прототип для екземплярів, створених за допомогою new.
Метод можна додати до прототипу після оголошення конструктора:
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.greet = function () {
return `Привіт, мене звати ${this.name}.`;
};
const firstUser = new User("Олена", 25);
const secondUser = new User("Андрій", 30);
console.log(firstUser.greet());
console.log(secondUser.greet());
console.log(firstUser.greet === secondUser.greet); // trueТепер greet існує лише один раз — у User.prototype. Обидва об’єкти використовують цей спільний метод.
Коли код звертається до властивості або методу об’єкта, JavaScript виконує пошук у кілька етапів:
Перевіряє сам об’єкт.
Якщо властивості немає, перевіряє прототип об’єкта.
Якщо властивості немає і в прототипі, перевіряє наступний прототип у ланцюжку.
Якщо властивість не знайдена, повертає undefined.
Для об’єкта, створеного через new User(), ланцюжок приблизно такий:
firstUser → User.prototype → Object.prototype → nullТому firstUser.greet() працює, навіть якщо greet не є власною властивістю firstUser.
Методи й властивості можна перевірити за допомогою hasOwnProperty:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Привіт, ${this.name}!`;
};
const user = new User("Олена");
console.log(user.hasOwnProperty("name")); // true
console.log(user.hasOwnProperty("greet")); // false
console.log("greet" in user); // trueВластивість name створена безпосередньо в об’єкті, тому вона є власною.
Метод greet знаходиться в прототипі. Він доступний через об’єкт, але не є його власною властивістю.
hasOwnProperty та inОператор in перевіряє весь ланцюжок прототипів:
console.log("name" in user); // true
console.log("greet" in user); // trueМетод hasOwnProperty перевіряє лише сам об’єкт:
console.log(user.hasOwnProperty("name")); // true
console.log(user.hasOwnProperty("greet")); // falsethis у методах прототипуМетод у прототипі виконується з тим самим контекстом this, що й метод, створений усередині конструктора.
function Product(name, price) {
this.name = name;
this.price = price;
}
Product.prototype.getInfo = function () {
return `${this.name}: ${this.price} грн`;
};
const product = new Product("Клавіатура", 2500);
console.log(product.getInfo()); // Клавіатура: 2500 грнПід час виклику:
product.getInfo();значення this дорівнює product.
Саме тому один і той самий метод може працювати з різними об’єктами:
const anotherProduct = new Product("Миша", 1200);
console.log(product.getInfo());
console.log(anotherProduct.getInfo());Метод один, але this у кожному виклику посилається на відповідний екземпляр.
До прототипу можна додати будь-яку кількість спільних методів:
function BankAccount(owner, balance = 0) {
this.owner = owner;
this.balance = balance;
}
BankAccount.prototype.deposit = function (amount) {
if (amount <= 0) {
return "Сума поповнення має бути більшою за нуль.";
}
this.balance += amount;
return `Рахунок поповнено. Баланс: ${this.balance} грн`;
};
BankAccount.prototype.withdraw = function (amount) {
if (amount <= 0) {
return "Сума зняття має бути більшою за нуль.";
}
if (amount > this.balance) {
return "Недостатньо коштів.";
}
this.balance -= amount;
return `Кошти знято. Баланс: ${this.balance} грн`;
};
BankAccount.prototype.getBalance = function () {
return `${this.owner}: ${this.balance} грн`;
};
const account = new BankAccount("Олена", 1000);
console.log(account.deposit(500));
console.log(account.withdraw(300));
console.log(account.getBalance());Властивості, які залежать від конкретного об’єкта, зазвичай створюють у конструкторі:
this.owner = owner;
this.balance = balance;Спільну поведінку для всіх екземплярів створюють у прототипі:
BankAccount.prototype.deposit = function () {
// ...
};function TodoList(title) {
this.title = title;
this.tasks = [];
}
TodoList.prototype.addTask = function (text) {
const task = {
text,
completed: false
};
this.tasks.push(task);
return task;
};
TodoList.prototype.completeTask = function (index) {
const task = this.tasks[index];
if (!task) {
return "Завдання не знайдено.";
}
task.completed = true;
return `Завдання "${task.text}" виконано.`;
};
TodoList.prototype.getPendingTasks = function () {
return this.tasks.filter(function (task) {
return !task.completed;
});
};
TodoList.prototype.getCompletedTasks = function () {
return this.tasks.filter(function (task) {
return task.completed;
});
};
const workTasks = new TodoList("Робочі завдання");
const homeTasks = new TodoList("Домашні завдання");
workTasks.addTask("Підготувати звіт");
workTasks.addTask("Перевірити пошту");
homeTasks.addTask("Прибрати кімнату");
console.log(workTasks.completeTask(0));
console.log(workTasks.getPendingTasks());
console.log(workTasks.getCompletedTasks());
console.log(workTasks.addTask === homeTasks.addTask); // true
console.log(workTasks.tasks === homeTasks.tasks); // falseУ цьому прикладі:
title і tasks належать конкретному екземпляру;
кожен список має власний масив tasks;
методи addTask, completeTask та інші спільні для всіх списків;
методи зберігаються в TodoList.prototype.
Важливо, що масив tasks створюється в конструкторі:
this.tasks = [];Якби масив створили в прототипі, усі екземпляри використовували б один спільний масив, що спричинило б помилки.
Власна властивість об’єкта має пріоритет над властивістю прототипу з таким самим іменем:
function User(name) {
this.name = name;
}
User.prototype.getRole = function () {
return "Користувач";
};
const admin = new User("Олена");
console.log(admin.getRole()); // Користувач
admin.getRole = function () {
return "Адміністратор";
};
console.log(admin.getRole()); // АдміністраторПісля присвоєння:
admin.getRole = function () {
return "Адміністратор";
};в об’єкті admin з’явився власний метод. Він перекриває метод із User.prototype, але не змінює поведінку інших екземплярів:
const regularUser = new User("Андрій");
console.log(regularUser.getRole()); // КористувачЯкщо змінити метод безпосередньо в прототипі, зміна буде доступна всім об’єктам, які не мають власного методу з таким самим іменем:
function User(name) {
this.name = name;
}
User.prototype.getGreeting = function () {
return `Привіт, ${this.name}!`;
};
const firstUser = new User("Олена");
const secondUser = new User("Андрій");
User.prototype.getGreeting = function () {
return `Вітаю, ${this.name}!`;
};
console.log(firstUser.getGreeting()); // Вітаю, Олена!
console.log(secondUser.getGreeting()); // Вітаю, Андрій!Обидва об’єкти знаходять метод у тому самому прототипі, тому після заміни вони використовують нову версію.
Object.assignЯкщо методів багато, їх можна додати до прототипу одним викликом:
function Counter(initialValue = 0) {
this.value = initialValue;
}
Object.assign(Counter.prototype, {
increment() {
this.value += 1;
},
decrement() {
this.value -= 1;
},
reset() {
this.value = 0;
},
getValue() {
return this.value;
}
});
const counter = new Counter(10);
counter.increment();
counter.increment();
console.log(counter.getValue()); // 12
counter.reset();
console.log(counter.getValue()); // 0Такий підхід зручний, коли потрібно згрупувати всі методи конструктора в одному місці.
Водночас у разі використання літерала об’єкта слід пам’ятати про можливу заміну прототипу:
function Counter() {
this.value = 0;
}
Counter.prototype = {
increment() {
this.value += 1;
}
};Після такого присвоєння властивість constructor більше не посилається автоматично на Counter. Якщо це важливо, її можна відновити:
Counter.prototype = {
constructor: Counter,
increment() {
this.value += 1;
}
};На практиці Object.assign часто зручніший, оскільки не замінює сам об’єкт прототипу.
constructorУ прототипі зазвичай є властивість constructor, яка посилається на функцію-конструктор:
function User(name) {
this.name = name;
}
const user = new User("Олена");
console.log(user.constructor === User); // trueПошук відбувається через прототип:
console.log(user.hasOwnProperty("constructor")); // false
console.log("constructor" in user); // trueЦе не означає, що кожен об’єкт має власну властивість constructor. Зазвичай він знаходить її в User.prototype.
Object.getPrototypeOf та isPrototypeOfДля перевірки прототипу можна використовувати Object.getPrototypeOf:
function User(name) {
this.name = name;
}
const user = new User("Олена");
console.log(Object.getPrototypeOf(user) === User.prototype); // trueТакож прототип можна перевірити методом isPrototypeOf:
console.log(User.prototype.isPrototypeOf(user)); // trueЦі перевірки допомагають зрозуміти, чи створений об’єкт певним конструктором і який прототип бере участь у пошуку властивостей.
Методи, що залежать від this, не варто створювати як стрілкові функції в прототипі:
function User(name) {
this.name = name;
}
User.prototype.greet = () => {
return `Привіт, ${this.name}`;
};
const user = new User("Олена");
console.log(user.greet());У стрілкової функції немає власного this. Вона використовує this із зовнішньої області видимості, тому в цьому прикладі this не буде посилатися на user.
Для методів конструктора використовуйте звичайні функції:
User.prototype.greet = function () {
return `Привіт, ${this.name}`;
};function User(name) {
this.name = name;
this.greet = function () {
return `Привіт, ${this.name}!`;
};
}Особливості:
кожен об’єкт отримує окрему функцію;
метод є власною властивістю об’єкта;
можна легко створити індивідуальну поведінку;
для великої кількості об’єктів може використовуватися більше пам’яті.
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Привіт, ${this.name}!`;
};Особливості:
усі екземпляри використовують одну функцію;
метод не є власною властивістю екземпляра;
менше дублювання;
зручно описувати спільну поведінку об’єктів.
Неправильно:
function Cart() {}
Cart.prototype.items = [];
const firstCart = new Cart();
const secondCart = new Cart();
firstCart.items.push("Книга");
console.log(secondCart.items); // ["Книга"]Обидва кошики використовують один масив із прототипу.
Правильно:
function Cart() {
this.items = [];
}
Cart.prototype.addItem = function (item) {
this.items.push(item);
};
const firstCart = new Cart();
const secondCart = new Cart();
firstCart.addItem("Книга");
console.log(firstCart.items); // ["Книга"]
console.log(secondCart.items); // []thisМетод може втратити контекст, якщо передати його як окрему функцію:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Привіт, ${this.name}!`;
};
const user = new User("Олена");
const greet = user.greet;
// У строгому режимі this буде undefined.
console.log(greet());Якщо метод потрібно передати окремо, прив’яжіть контекст за допомогою bind:
const boundGreet = user.greet.bind(user);
console.log(boundGreet()); // Привіт, Олена!Неправильно:
User.prototype.greet = () => {
return this.name;
};Стрілкова функція не отримує this від об’єкта під час виклику.
Правильно:
User.prototype.greet = function () {
return this.name;
};Якщо створити в екземплярі властивість із таким самим іменем, вона перекриє метод прототипу:
const user = new User("Олена");
user.greet = "Текст";
console.log(user.greet); // ТекстПісля цього виклик user.greet() спричинить помилку, оскільки greet більше не є функцією для цього об’єкта.
Функція-конструктор створює властивості конкретного екземпляра.
Властивість prototype містить спільні властивості та методи для екземплярів.
Метод у прототипі створюється один раз і використовується багатьма об’єктами.
Власні властивості об’єкта мають пріоритет над властивостями прототипу.
hasOwnProperty перевіряє лише власні властивості об’єкта.
Оператор in перевіряє об’єкт і весь ланцюжок його прототипів.
Змінювані дані, наприклад масиви та об’єкти, зазвичай потрібно створювати в конструкторі.
Для методів, які використовують this, слід застосовувати звичайні функції, а не стрілкові.