ООП: Классы и объекты в JavaScript (Node.js): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты решил построить город из конструктора LEGO. У тебя есть коробка с деталями, и ты можешь собрать из них всё что угодно: домик, машинку, дерево. Но каждый раз, когда тебе нужна новая машинка, ты не разбираешь старую и не собираешь заново — ты просто берёшь инструкцию (чертёж) и по ней собираешь ещё одну такую же машинку.
В программировании класс — это и есть такой чертёж или инструкция. А объект — это конкретная машинка, собранная по этому чертежу. Если чертёж говорит, что у машинки должны быть колёса, руль и фары, то каждая собранная машинка будет иметь эти детали. Но цвет фар или размер колёс у разных машинок может отличаться — это уже свойства объекта.
Зачем это нужно? Представь, что ты пишешь программу для интернет-магазина. У тебя есть тысячи товаров. Каждый товар имеет название, цену, описание, картинку. Если бы ты писал код без ООП, тебе пришлось бы для каждого товара создавать кучу отдельных переменных и функций. А с классами ты один раз описываешь «чертёж товара», а потом создаёшь столько объектов-товаров, сколько нужно. Это экономит время, уменьшает количество ошибок и делает код понятным.
💡 Важно: ООП (Объектно-Ориентированное Программирование) — это не какой-то особый язык, а способ думать о программе. Ты учишься не просто писать код, а проектировать систему из взаимодействующих «кирпичиков» — объектов.
Теория: как это работает изнутри
Давай заглянем «под капот» JavaScript и разберёмся, что на самом деле происходит, когда мы создаём класс и объект.
Что такое класс на самом деле?
В JavaScript класс — это функция особого вида. Когда ты пишешь class User { ... }, JavaScript создаёт функцию с именем User. Но эта функция не обычная — у неё есть специальное свойство prototype, в котором хранятся все методы класса.
Что такое объект?
Объект — это набор свойств (ключ: значение). Когда ты создаёшь объект через new User(), происходит следующее:
- Создаётся пустой объект
{} - К этому объекту привязывается прототип (ссылка на
User.prototype) - Вызывается функция-конструктор (метод
constructor), которая заполняет объект данными - Объект возвращается и сохраняется в переменную
Прототипное наследование
В JavaScript нет «настоящих» классов, как в Java или C++. Здесь используется прототипное наследование. Это значит, что если у объекта нет какого-то свойства, JavaScript ищет его в прототипе (родителе), потом в прототипе прототипа и так далее, пока не найдёт или не дойдёт до null.
// Пример прототипной цепочки
const animal = {
eats: true
};
const rabbit = {
jumps: true
};
// Устанавливаем прототип: rabbit наследует от animal
rabbit.__proto__ = animal;
console.log(rabbit.jumps); // true — своё свойство
console.log(rabbit.eats); // true — взято из прототипа animal
⚠️ Важно: Свойство__proto__— устаревшее. В современном JS используютObject.getPrototypeOf()иObject.setPrototypeOf(), а для классов —extends.
Как JavaScript хранит методы?
Методы класса не копируются в каждый объект. Они хранятся в одном месте — в ClassName.prototype. Все объекты одного класса ссылаются на один и тот же прототип. Это экономит память: если у тебя 1000 объектов, методы не дублируются 1000 раз.
Синтаксис в JavaScript (Node.js): все формы и варианты
В JavaScript есть несколько способов создать класс и объект. Давай рассмотрим все основные варианты.
1. Классический синтаксис (ES6+)
Самый современный и понятный способ — использовать ключевое слово class.
// Объявляем класс
class Book {
// Конструктор — вызывается при создании объекта
constructor(title, author, year) {
this.title = title; // this — ссылка на создаваемый объект
this.author = author;
this.year = year;
this.isAvailable = true; // свойство по умолчанию
}
// Метод класса
getInfo() {
return `${this.title} — ${this.author} (${this.year})`;
}
// Ещё один метод
borrow() {
if (this.isAvailable) {
this.isAvailable = false;
return `Вы взяли книгу "${this.title}"`;
} else {
return `Книга "${this.title}" уже занята`;
}
}
}
// Создаём объекты (экземпляры класса)
const book1 = new Book('Война и мир', 'Лев Толстой', 1869);
const book2 = new Book('Преступление и наказание', 'Фёдор Достоевский', 1866);
console.log(book1.getInfo()); // "Война и мир — Лев Толстой (1869)"
console.log(book1.borrow()); // "Вы взяли книгу "Война и мир""
console.log(book1.borrow()); // "Книга "Война и мир" уже занята"
2. Функция-конструктор (старый способ, ES5)
До появления class в JavaScript использовали обычные функции.
// Функция-конструктор
function Car(brand, model, year) {
this.brand = brand;
this.model = model;
this.year = year;
this.mileage = 0;
}
// Методы добавляются в prototype
Car.prototype.getFullName = function() {
return `${this.brand} ${this.model} (${this.year})`;
};
Car.prototype.drive = function(km) {
this.mileage += km;
return `Пробег теперь: ${this.mileage} км`;
};
// Создание объекта
const myCar = new Car('Toyota', 'Camry', 2020);
console.log(myCar.getFullName()); // "Toyota Camry (2020)"
console.log(myCar.drive(150)); // "Пробег теперь: 150 км"
3. Класс-выражение (class expression)
Класс можно сохранить в переменную, как функцию.
// Анонимный класс
const Animal = class {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} издаёт звук`;
}
};
// Именованный класс (имя видно только внутри класса)
const Plant = class PlantClass {
constructor(species) {
this.species = species;
}
getSpecies() {
return this.species;
}
};
const dog = new Animal('Бобик');
console.log(dog.speak()); // "Бобик издаёт звук"
4. Статические методы и свойства
Методы, которые принадлежат самому классу, а не его объектам.
class MathHelper {
// Статическое свойство
static PI = 3.14159;
// Статический метод
static circleArea(radius) {
return this.PI * radius * radius;
}
// Обычный метод
multiply(a, b) {
return a * b;
}
}
// Вызов статического метода — через класс
console.log(MathHelper.circleArea(5)); // 78.53975
// Обычный метод — только через объект
const helper = new MathHelper();
console.log(helper.multiply(3, 4)); // 12
// Ошибка: статический метод нельзя вызвать у объекта
// console.log(helper.circleArea(5)); // TypeError
5. Приватные поля (современный синтаксис)
Свойства, которые нельзя прочитать или изменить снаружи класса.
class BankAccount {
// Приватное поле (начинается с #)
#balance = 0;
#pin;
constructor(owner, pin) {
this.owner = owner;
this.#pin = pin;
}
// Публичный метод для доступа к приватному полю
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return `Баланс пополнен. Текущий баланс: ${this.#balance}`;
}
return 'Сумма должна быть положительной';
}
getBalance(pin) {
if (pin === this.#pin) {
return `Баланс: ${this.#balance}`;
}
return 'Неверный PIN';
}
}
const account = new BankAccount('Иван', 1234);
console.log(account.deposit(1000)); // "Баланс пополнен. Текущий баланс: 1000"
console.log(account.getBalance(1234)); // "Баланс: 1000"
// console.log(account.#balance); // SyntaxError — приватное поле!
6. Геттеры и сеттеры
Специальные методы, которые выглядят как свойства.
class Temperature {
constructor(celsius) {
this._celsius = celsius; // _ — соглашение, что поле "защищённое"
}
// Геттер — получаем значение
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
// Сеттер — устанавливаем значение с проверкой
set fahrenheit(value) {
if (value < -459.67) {
throw new Error('Температура ниже абсолютного нуля!');
}
this._celsius = (value - 32) * 5/9;
}
// Обычный геттер для celsius
get celsius() {
return this._celsius;
}
}
const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77 — используем геттер
temp.fahrenheit = 100; // используем сеттер
console.log(temp.celsius); // 37.777...
7. Наследование (extends)
Создание класса на основе другого класса.
// Базовый класс
class Vehicle {
constructor(brand) {
this.brand = brand;
this.speed = 0;
}
accelerate(amount) {
this.speed += amount;
return `Скорость: ${this.speed} км/ч`;
}
stop() {
this.speed = 0;
return 'Остановились';
}
}
// Дочерний класс
class ElectricCar extends Vehicle {
constructor(brand, batteryCapacity) {
super(brand); // Вызов конструктора родителя
this.batteryCapacity = batteryCapacity;
this.charge = 100;
}
// Переопределяем метод родителя
accelerate(amount) {
if (this.charge > 0) {
this.charge -= amount * 0.5;
return super.accelerate(amount) + ` | Заряд: ${this.charge}%`;
}
return 'Нет заряда!';
}
// Новый метод
chargeBattery() {
this.charge = 100;
return 'Батарея заряжена';
}
}
const tesla = new ElectricCar('Tesla', 100);
console.log(tesla.accelerate(50)); // "Скорость: 50 км/ч | Заряд: 75%"
console.log(tesla.chargeBattery()); // "Батарея заряжена"
console.log(tesla.stop()); // "Остановились" — метод родителя
Примеры кода — от простого к сложному
Пример 1: Класс «Студент» (базовый)
// Простейший класс с одним методом
class Student {
constructor(name, grade) {
this.name = name;
this.grade = grade;
}
introduce() {
return `Привет, я ${this.name}, учусь в ${this.grade} классе`;
}
}
const student1 = new Student('Маша', 7);
console.log(student1.introduce()); // "Привет, я Маша, учусь в 7 классе"
Пример 2: Класс «Счётчик» (с изменяемым состоянием)
class Counter {
constructor(startValue = 0) {
this.count = startValue;
}
increment() {
this.count++;
return this.count;
}
decrement() {
this.count--;
return this.count;
}
reset() {
this.count = 0;
return 'Счётчик сброшен';
}
}
const counter = new Counter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.reset()); // "Счётчик сброшен"
Пример 3: Класс «Библиотека» (работа с массивами объектов)
class Library {
constructor(name) {
this.name = name;
this.books = []; // Массив для хранения книг
}
addBook(title, author) {
const book = {
title: title,
author: author,
id: Date.now(), // Уникальный ID на основе времени
isAvailable: true
};
this.books.push(book);
return `Книга "${title}" добавлена в библиотеку`;
}
findBook(title) {
return this.books.find(book => book.title === title);
}
borrowBook(title) {
const book = this.findBook(title);
if (book && book.isAvailable) {
book.isAvailable = false;
return `Вы взяли книгу "${title}"`;
}
return `Книга "${title}" недоступна`;
}
listAvailableBooks() {
return this.books
.filter(book => book.isAvailable)
.map(book => book.title);
}
}
const myLibrary = new Library('Школьная библиотека');
myLibrary.addBook('Гарри Поттер', 'Дж. Роулинг');
myLibrary.addBook('Властелин колец', 'Дж. Толкин');
console.log(myLibrary.borrowBook('Гарри Поттер')); // "Вы взяли книгу..."
console.log(myLibrary.listAvailableBooks()); // ["Властелин колец"]
Пример 4: Наследование — «Птицы»
class Bird {
constructor(name) {
this.name = name;
}
fly() {
return `${this.name} летит`;
}
eat() {
return `${this.name} ест`;
}
}
class Penguin extends Bird {
constructor(name) {
super(name);
this.canFly = false;
}
// Переопределяем метод fly
fly() {
return `${this.name} не умеет летать, но умеет плавать!`;
}
swim() {
return `${this.name} плывёт`;
}
}
class Eagle extends Bird {
constructor(name, wingspan) {
super(name);
this.wingspan = wingspan;
}
fly() {
return `${this.name} парит в небе с размахом крыльев ${this.wingspan} м`;
}
}
const penguin = new Penguin('Пингвин');
const eagle = new Eagle('Орёл', 2.5);
console.log(penguin.fly()); // "Пингвин не умеет летать..."
console.log(penguin.swim()); // "Пингвин плывёт"
console.log(eagle.fly()); // "Орёл парит в небе..."
console.log(eagle.eat()); // "Орёл ест" — метод родителя
Пример 5: Композиция — «Автомобиль и Двигатель»
// Композиция: один класс содержит объект другого класса
class Engine {
constructor(type, horsepower) {
this.type = type;
this.horsepower = horsepower;
this.isRunning = false;
}
start() {
this.isRunning = true;
return 'Двигатель запущен';
}
stop() {
this.isRunning = false;
return 'Двигатель остановлен';
}
}
class Car {
constructor(brand, model, engineType, horsepower) {
this.brand = brand;
this.model = model;
// Композиция: создаём двигатель внутри автомобиля
this.engine = new Engine(engineType, horsepower);
}
startCar() {
return this.engine.start();
}
stopCar() {
return this.engine.stop();
}
getInfo() {
return `${this.brand} ${this.model} с ${this.engine.type} двигателем (${this.engine.horsepower} л.с.)`;
}
}
const myCar = new Car('BMW', 'X5', 'бензиновый', 340);
console.log(myCar.getInfo()); // "BMW X5 с бензиновым двигателем (340 л.с.)"
console.log(myCar.startCar()); // "Двигатель запущен"
Пример 6: Полиморфизм — «Фигуры»
// Полиморфизм: разные классы реализуют одинаковые методы по-своему
class Shape {
getArea() {
return 0; // Базовый метод
}
getDescription() {
return 'Это фигура';
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
getArea() {
return Math.PI * this.radius * this.radius;
}
getDescription() {
return `Круг радиусом ${this.radius}`;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
getDescription() {
return `Прямоугольник ${this.width}×${this.height}`;
}
}
// Функция, которая работает с любыми фигурами
function printShapeInfo(shape) {
console.log(`${shape.getDescription()}: площадь = ${shape.getArea()}`);
}
const shapes = [
new Circle(5),
new Rectangle(4, 6),
new Circle(10)
];
shapes.forEach(shape => printShapeInfo(shape));
// "Круг радиусом 5: площадь = 78.539..."
// "Прямоугольник 4×6: площадь = 24"
// "Круг радиусом 10: площадь = 314.159..."
Пример 7: Класс с валидацией данных
class User {
#password;
#email;
constructor(name, email, password) {
this.name = name;
this.#setEmail(email); // Используем приватный сеттер
this.#setPassword(password);
}
// Приватный метод для валидации email
#setEmail(email) {
if (!email.includes('@') || !email.includes('.')) {
throw new Error('Некорректный email');
}
this.#email = email;
}
// Приватный метод для валидации пароля
#setPassword(password) {
if (password.length < 6) {
throw new Error('Пароль должен быть минимум 6 символов');
}
if (!/[A-Z]/.test(password)) {
throw new Error('Пароль должен содержать заглавную букву');
}
this.#password = password;
}
// Публичный метод для смены пароля
changePassword(oldPassword, newPassword) {
if (oldPassword !== this.#password) {
return 'Неверный старый пароль';
}
try {
this.#setPassword(newPassword);
return 'Пароль успешно изменён';
} catch (error) {
return error.message;
}
}
getEmail() {
// Маскируем email для безопасности
const [name, domain] = this.#email.split('@');
return `${name[0]}***@${domain}`;
}
}
const user = new User('Иван', 'ivan@mail.ru', 'Password1');
console.log(user.getEmail()); // "и***@mail.ru"
console.log(user.changePassword('Password1', 'NewPass123')); // "Пароль успешно изменён"
console.log(user.changePassword('Wrong', 'Short1')); // "Неверный старый пароль"
Сравнение с Python: чем похоже и чем отличается
| Характеристика | JavaScript (Node.js) | Python |
|---|---|---|
| Объявление класса | class MyClass { ... } |
class MyClass: ... |
| Конструктор | constructor() |
def __init__(self): |
| Ссылка на текущий объект | this |
self |
| Приватные поля | #field (настоящие приватные) |
__field (name mangling, не настоящие) |
| Статические методы | static method() {} |
@staticmethod или @classmethod |
| Наследование | class Child extends Parent |
class Child(Parent): |
| Вызов родительского метода | super.method() |
super().method() |
| Геттеры/сеттеры | get prop() / set prop() |
@property / @prop.setter |
| Множественное наследование | Нет (только через примеси) | Да (class C(A, B):) |
| Типизация | Динамическая, слабая | Динамическая, строгая |
| Прототипное наследование | Да (основа всего) | Нет (классическое) |
💡 Совет: Если ты знаешь Python, то в JavaScript тебе будет непривычно, чтоthisможет менять своё значение в зависимости от контекста вызова. В Pythonselfвсегда явно передаётся первым аргументом.
Типичные ошибки и подводные камни
Ошибка 1: Забыли ключевое слово new
class User {
constructor(name) {
this.name = name;
}
}
// Ошибка! Забыли new
const user = User('Иван'); // TypeError: Class constructor User cannot be invoked without 'new'
Ошибка 2: Потеря контекста this
class Button {
constructor(text) {
this.text = text;
}
click() {
console.log(`Нажата кнопка: ${this.text}`);
}
}
const btn = new Button('Отправить');
// Правильно
btn.click(); // "Нажата кнопка: Отправить"
// Ошибка! Потеря контекста
const clickHandler = btn.click;
clickHandler(); // "Нажата кнопка: undefined" — this потерян!
// Решение: стрелочная функция или bind
const correctHandler = () => btn.click();
correctHandler(); // "Нажата кнопка: Отправить"
Ошибка 3: Изменение прототипа после создания объектов
class Animal {
constructor(name) {
this.name = name;
}
}
const dog = new Animal('Бобик');
// Меняем прототип после создания объекта
Animal.prototype.speak = function() {
return `${this.name} говорит`;
};
console.log(dog.speak()); // "Бобик говорит" — работает, но это плохая практика
// Опасность: если изменить метод, который уже используется
const originalSpeak = Animal.prototype.speak;
Animal.prototype.speak = function() {
return 'Хакнутый метод!';
};
console.log(dog.speak()); // "Хакнутый метод!" — все объекты изменились
Ошибка 4: Путаница между статическими и обычными методами
class MathUtils {
static add(a, b) {
return a + b;
}
multiply(a, b) {
return a * b;
}
}
// Правильно
console.log(MathUtils.add(2, 3)); // 5
// Ошибка: статический метод вызывается у объекта
const utils = new MathUtils();
// console.log(utils.add(2, 3)); // TypeError: utils.add is not a function
// Ошибка: обычный метод вызывается у класса
// console.log(MathUtils.multiply(2, 3)); // TypeError: MathUtils.multiply is not a function
Ошибка 5: Неправильное использование super()
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
// Ошибка: super() должен быть вызван до использования this
// this.age = age; // ReferenceError: Must call super constructor
super(name); // Правильно: сначала super()
this.age = age;
}
}
Ошибка 6: Создание методов внутри конструктора
class BadPractice {
constructor(name) {
this.name = name;
// Плохо: метод создаётся заново для каждого объекта
this.sayHello = function() {
return `Привет, я ${this.name}`;
};
}
}
class GoodPractice {
constructor(name) {
this.name = name;
}
// Хорошо: метод в прототипе, один для всех объектов
sayHello() {
return `Привет, я ${this.name}`;
}
}
const bad1 = new BadPractice('Анна');
const bad2 = new BadPractice('Иван');
console.log(bad1.sayHello === bad2.sayHello); // false — разные функции!
const good1 = new GoodPractice('Анна');
const good2 = new GoodPractice('Иван');
console.log(good1.sayHello === good2.sayHello); // true — одна функция!
Лучшие практики и советы профессионалов
1. Принцип единственной ответственности (Single Responsibility)
Класс должен делать только одну вещь и делать её хорошо. Если класс начинает «раздуваться» — разбей его на несколько.
// Плохо: класс делает всё
class UserManager {
constructor(user) {
this.user = user;
}
saveToDatabase() { /* ... */ }
sendEmail() { /* ... */ }
generateReport() { /* ... */ }
}
// Хорошо: разделяем ответственность
class User {
constructor(data) { this.data = data; }
}
class UserRepository {
save(user) { /* ... */ }
}
class EmailService {
sendEmail(user, message) { /* ... */ }
}
class ReportGenerator {
generate(user) { /* ... */ }
}
2. Используйте геттеры и сеттеры для контроля доступа
Не делайте все свойства публичными. Используйте приватные поля + геттеры/сеттеры.
class Product {
#price;
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;
}
get discountedPrice() {
return this.#price * 0.9; // 10% скидка
}
}
3. Избегайте глубокого наследования
Наследование в 3+ уровня — это «запах кода». Используйте композицию вместо наследования.
// Плохо: глубокое наследование
class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}
class Poodle extends Dog {}
// Хорошо: композиция
class Animal {
constructor(behavior) {
this.behavior = behavior;
}
}
class Behavior {
constructor(sound, movement) {
this.sound = sound;
this.movement = movement;
}
}
const dogBehavior = new Behavior('Гав', 'бегает');
const dog = new Animal(dogBehavior);
4. Используйте фабричные методы для сложной логики создания
class User {
constructor(name, role, permissions) {
this.name = name;
this.role = role;
this.permissions = permissions;
}
// Фабричный метод
static createAdmin(name) {
return new User(name, 'admin', ['read', 'write', 'delete']);
}
static createGuest(name) {
return new User(name, 'guest', ['read']);
}
}
const admin = User.createAdmin('Иван');
const guest = User.createGuest('Пётр');
5. Документируйте классы с помощью JSDoc
/**
* Класс для работы с задачами
* @class
*/
class Task {
/**
* Создаёт новую задачу
* @param {string} title - Название задачи
* @param {string} description - Описание задачи
*/
constructor(title, description) {
this.title = title;
this.description = description;
this.completed = false