⚡ JavaScript

ООП: Классы и объекты в JavaScript (Node.js): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты решил построить город из конструктора LEGO. У тебя есть коробка с деталями, и ты можешь собрать из них всё что угодно: домик, машинку, дерево. Но каждый раз, когда тебе нужна новая машинка, ты не разбираешь старую и не собираешь заново — ты просто берёшь инструкцию (чертёж) и по ней собираешь ещё одну такую же машинку.

В программировании класс — это и есть такой чертёж или инструкция. А объект — это конкретная машинка, собранная по этому чертежу. Если чертёж говорит, что у машинки должны быть колёса, руль и фары, то каждая собранная машинка будет иметь эти детали. Но цвет фар или размер колёс у разных машинок может отличаться — это уже свойства объекта.

Зачем это нужно? Представь, что ты пишешь программу для интернет-магазина. У тебя есть тысячи товаров. Каждый товар имеет название, цену, описание, картинку. Если бы ты писал код без ООП, тебе пришлось бы для каждого товара создавать кучу отдельных переменных и функций. А с классами ты один раз описываешь «чертёж товара», а потом создаёшь столько объектов-товаров, сколько нужно. Это экономит время, уменьшает количество ошибок и делает код понятным.

💡 Важно: ООП (Объектно-Ориентированное Программирование) — это не какой-то особый язык, а способ думать о программе. Ты учишься не просто писать код, а проектировать систему из взаимодействующих «кирпичиков» — объектов.

Теория: как это работает изнутри

Давай заглянем «под капот» JavaScript и разберёмся, что на самом деле происходит, когда мы создаём класс и объект.

Что такое класс на самом деле?

В JavaScript класс — это функция особого вида. Когда ты пишешь class User { ... }, JavaScript создаёт функцию с именем User. Но эта функция не обычная — у неё есть специальное свойство prototype, в котором хранятся все методы класса.

Что такое объект?

Объект — это набор свойств (ключ: значение). Когда ты создаёшь объект через new User(), происходит следующее:

  1. Создаётся пустой объект {}
  2. К этому объекту привязывается прототип (ссылка на User.prototype)
  3. Вызывается функция-конструктор (метод constructor), которая заполняет объект данными
  4. Объект возвращается и сохраняется в переменную

Прототипное наследование

В 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 может менять своё значение в зависимости от контекста вызова. В Python self всегда явно передаётся первым аргументом.

Типичные ошибки и подводные камни

Ошибка 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