Прототипи та класи
JavaScript прототипи та класи — родинне дерево вирізьблене на дерев'яній дошці з трьома поколіннями
JavaScript -- унікальна мова: наслідування в ній працює через прототипи, а не через класи, як у Java чи C#. ES6 додав синтаксис class, але під капотом -- все ті ж прототипи. У цьому уроці розберемось, як це працює, і навчимось писати ООП-код на JavaScript.
Прототипний ланцюжок (Prototype Chain)
Кожен об'єкт у JavaScript має прихований зв'язок з іншим об'єктом -- своїм прототипом. Коли ти звертаєшся до властивості, якої немає в самому об'єкті, JavaScript шукає її у прототипі:
const user = { name: "Олексій" };
// toString() не визначений в user, але працює!
console.log(user.toString()); // "[object Object]"
Звідки toString()? З прототипу Object.prototype, який є у кожного об'єкта.
// Подивитись на прототип
console.log(Object.getPrototypeOf(user)); // Object.prototype
console.log(Object.getPrototypeOf(user) === Object.prototype); // true
Ланцюжок пошуку
user → Object.prototype → null
JavaScript шукає властивість по ланцюжку, поки не знайде або не дійде до null.
Властивість __proto__ -- застарілий спосіб доступу до прототипу. Використовуй Object.getPrototypeOf(obj) для читання та Object.create(proto) для створення об'єкта з певним прототипом.
Створення об'єктів з прототипом
const animal = {
eat() {
console.log(`${this.name} їсть`);
},
sleep() {
console.log(`${this.name} спить`);
},
};
// Створюємо об'єкт з animal як прототипом
const cat = Object.create(animal);
cat.name = "Мурчик";
cat.meow = function() {
console.log(`${this.name}: Мяу!`);
};
cat.eat(); // "Мурчик їсть" (метод з прототипу)
cat.meow(); // "Мурчик: Мяу!" (власний метод)
cat.sleep(); // "Мурчик спить" (метод з прототипу)
Це основа наслідування в JavaScript. Але писати так щоразу незручно. Саме тому з'явились класи.
ES6 Classes -- синтаксичний цукор
Класи -- зручний синтаксис для створення об'єктів з прототипами:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Привіт, я ${this.name}!`;
}
getInfo() {
return `${this.name} (${this.email})`;
}
}
const user = new User("Олексій", "alex@test.com");
console.log(user.greet()); // "Привіт, я Олексій!"
console.log(user.getInfo()); // "Олексій (alex@test.com)"
Що робить new?
Коли ти пишеш new User(...), JavaScript:
- Створює порожній об'єкт
{} - Встановлює йому прототип
User.prototype - Викликає
constructorзthis= новий об'єкт - Повертає цей об'єкт
// Перевірка
console.log(user instanceof User); // true
console.log(typeof User); // "function" (класи -- це функції!)
Під капотом class User { ... } створює функцію-конструктор і додає методи до User.prototype. Тобто class -- це буквально "синтаксичний цукор" над прототипами. Але синтаксис класів набагато зрозуміліший, тому використовуй саме його.
Getters та Setters
Дозволяють контролювати доступ до властивостей:
class Product {
constructor(name, price) {
this.name = name;
this._price = price; // конвенція: _ означає "приватне"
}
get price() {
return `${this._price} грн`;
}
set price(value) {
if (value < 0) {
throw new Error("Ціна не може бути від'ємною");
}
this._price = value;
}
}
const item = new Product("Книга", 250);
console.log(item.price); // "250 грн" (getter)
item.price = 300; // setter
console.log(item.price); // "300 грн"
// item.price = -10; // Error: Ціна не може бути від'ємною
Наслідування: extends та super
extends дозволяє створити клас на основі іншого:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Привіт, я ${this.name}!`;
}
getRole() {
return "user";
}
}
class Admin extends User {
constructor(name, email, permissions) {
super(name, email); // виклик конструктора батьківського класу
this.permissions = permissions;
}
getRole() {
return "admin"; // перевизначення (override) методу
}
hasPermission(action) {
return this.permissions.includes(action);
}
}
const admin = new Admin("Марія", "maria@test.com", ["delete", "edit"]);
console.log(admin.greet()); // "Привіт, я Марія!" (від User)
console.log(admin.getRole()); // "admin" (перевизначений)
console.log(admin.hasPermission("delete")); // true
console.log(admin instanceof Admin); // true
console.log(admin instanceof User); // true
У конструкторі дочірнього класу super() обов'язково викликається перед використанням this. Інакше буде помилка ReferenceError.
Виклик методів батька через super
class Admin extends User {
greet() {
const baseGreeting = super.greet(); // виклик greet() з User
return `${baseGreeting} Я адміністратор.`;
}
}
const admin = new Admin("Марія", "maria@test.com", []);
console.log(admin.greet());
// "Привіт, я Марія! Я адміністратор."
Статичні методи та властивості
static -- методи і властивості, що належать класу, а не екземплярам:
class MathHelper {
static PI = 3.14159;
static square(x) {
return x * x;
}
static clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
}
// Викликаються через клас, НЕ через екземпляр
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.square(5)); // 25
console.log(MathHelper.clamp(15, 0, 10)); // 10
Типовий приклад -- фабричний метод:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
static fromJSON(json) {
const data = JSON.parse(json);
return new User(data.name, data.email);
}
}
const user = User.fromJSON('{"name":"Олексій","email":"alex@test.com"}');
console.log(user.name); // "Олексій"
Приватні поля (#)
ES2022 додав справжні приватні поля -- вони недоступні ззовні класу:
class BankAccount {
#balance; // приватне поле
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.#balance); // SyntaxError!
Приватні поля # -- справжня приватність, на відміну від конвенції з _ (underscore). Використовуй # для чутливих даних. Конвенцію _ -- для "не чіпай це" підказки іншим розробникам.
instanceof -- перевірка типу
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const dog = new Dog();
const cat = new Cat();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Cat); // false
// Працює з вбудованими типами
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
Практика: система користувачів
class User {
#password;
constructor(name, email, password) {
this.name = name;
this.email = email;
this.#password = password;
this.createdAt = new Date();
}
checkPassword(input) {
return input === this.#password;
}
getInfo() {
return `${this.name} (${this.email})`;
}
static createGuest() {
return new User("Гість", "guest@example.com", "");
}
}
class Admin extends User {
#permissions;
constructor(name, email, password, permissions = []) {
super(name, email, password);
this.#permissions = permissions;
}
hasPermission(action) {
return this.#permissions.includes(action);
}
getInfo() {
return `[Admin] ${super.getInfo()}`;
}
}
// Використання
const user = new User("Олексій", "alex@test.com", "secret123");
const admin = new Admin("Марія", "maria@test.com", "admin456", ["delete", "ban"]);
const guest = User.createGuest();
console.log(user.getInfo()); // "Олексій (alex@test.com)"
console.log(admin.getInfo()); // "[Admin] Марія (maria@test.com)"
console.log(admin.hasPermission("delete")); // true
console.log(guest.getInfo()); // "Гість (guest@example.com)"
console.log(user.checkPassword("secret123")); // true
Класи vs прості об'єкти: коли що?
| Ситуація | Що використати |
|---|---|
| Прості дані (config, options) | Об'єкт { } |
| Одноразова структура | Об'єкт { } |
| Багато екземплярів одного типу | class |
| Потрібне наслідування | class з extends |
| Потрібна інкапсуляція (приватні поля) | class з # |
| Набір утиліт без стану | Модуль з функціями (урок 6.6) |
У React (Block 8) компоненти пишуть як функції, а не класи. Але класи залишаються важливими для моделей даних, сервісів, бібліотек та патернів на кшталт Singleton. У TypeScript (Block 7) класи стануть ще потужнішими завдяки типізації.
Підсумок
- Прототип -- об'єкт, в якому JavaScript шукає властивості, якщо їх немає в поточному об'єкті
- class -- зручний синтаксис для створення об'єктів з прототипами
- constructor -- метод, що викликається при
new - extends -- наслідування, super -- виклик батьківського конструктора/методу
- static -- методи та властивості, що належать класу
- #поле -- справжні приватні поля (ES2022)
- instanceof -- перевірка, чи є об'єкт екземпляром класу
- Класи -- "синтаксичний цукор" над прототипами
Блок JavaScript Advanced завершено! Далі -- Block 7: TypeScript.
Корисні посилання:
- javascript.info: Прототипне наслідування -- основи прототипів (українською)
- javascript.info: Класи -- повний розділ про класи (українською)
- MDN: Classes -- документація ES6 класів
- MDN: Inheritance and the prototype chain -- глибоке занурення в прототипи