🎉 Первое видео: Интервью с разработчиком из Meta
Решать JS задачи

ООП в JavaScript (Объектно-ориентированное программирование)

Объектно-ориентированное программирование (ООП) — это парадигма программирования, основанная на концепции «объектов», которые содержат данные (свойства) и код (методы). ООП помогает структурировать код, делает его более понятным, переиспользуемым и легким в поддержке.

JavaScript поддерживает ООП, но реализует его немного иначе, чем классические объектно-ориентированные языки (Java, C++). До ES6 (2015) ООП в JavaScript строилось на прототипах, а с ES6 появился синтаксис классов, который является синтаксическим сахаром над прототипным наследованием.

Основные принципы ООП

1

Инкапсуляция (Encapsulation)

Объединение данных и методов, работающих с этими данными, в единую сущность (объект или класс). Скрытие внутренней реализации и предоставление публичного интерфейса для взаимодействия.

2

Наследование (Inheritance)

Механизм, позволяющий создавать новые классы на основе существующих, наследуя их свойства и методы. Это способствует переиспользованию кода и созданию иерархий классов.

3

Полиморфизм (Polymorphism)

Способность объектов с одинаковым интерфейсом иметь различную реализацию. Один и тот же метод может вести себя по-разному в зависимости от объекта, который его вызывает.

4

Абстракция (Abstraction)

Выделение главных, наиболее значимых характеристик объекта и игнорирование второстепенных. Упрощение сложных систем путем моделирования классов, соответствующих проблемной области.

Классы в JavaScript (ES6+)

С появлением ES6 в JavaScript добавлен синтаксис классов, который делает код более читаемым и привычным для разработчиков, знакомых с классическим ООП.

Объявление класса

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Привет, меня зовут ${this.name}, мне ${this.age} лет`);
  }

  celebrateBirthday() {
    this.age++;
    console.log(`С днём рождения! Теперь мне ${this.age} лет`);
  }
}

const person = new Person('Алексей', 25);
person.greet();
person.celebrateBirthday();

Компоненты класса:

  • constructor — специальный метод для инициализации объекта
  • this — ссылка на текущий экземпляр класса
  • Методы класса — функции, доступные всем экземплярам

Инкапсуляция

Инкапсуляция позволяет скрыть внутренние детали реализации и предоставить только необходимый интерфейс для взаимодействия.

Приватные поля (ES2022)

class BankAccount {
  #balance = 0;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Пополнено на ${amount}. Баланс: ${this.#balance}`);
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Снято ${amount}. Баланс: ${this.#balance}`);
    } else {
      console.log('Недостаточно средств');
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance());

Приватные поля (с префиксом #) недоступны извне класса. Это обеспечивает настоящую инкапсуляцию.

Геттеры и сеттеры

class Rectangle {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }

  get area() {
    return this._width * this._height;
  }

  get perimeter() {
    return 2 * (this._width + this._height);
  }

  set width(value) {
    if (value > 0) {
      this._width = value;
    } else {
      console.log('Ширина должна быть положительной');
    }
  }

  set height(value) {
    if (value > 0) {
      this._height = value;
    } else {
      console.log('Высота должна быть положительной');
    }
  }
}

const rect = new Rectangle(10, 5);
console.log(rect.area);
rect.width = 15;
console.log(rect.area);

Наследование

Наследование позволяет создавать новые классы на основе существующих, расширяя или изменяя их функциональность.

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} издаёт звук`);
  }

  move() {
    console.log(`${this.name} двигается`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} лает: Гав-гав!`);
  }

  fetch() {
    console.log(`${this.name} приносит мяч`);
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name);
    this.color = color;
  }

  speak() {
    console.log(`${this.name} мяукает: Мяу!`);
  }

  scratch() {
    console.log(`${this.name} точит когти`);
  }
}

const dog = new Dog('Бобик', 'Лабрадор');
dog.speak();
dog.move();
dog.fetch();

const cat = new Cat('Мурка', 'Рыжий');
cat.speak();
cat.move();
cat.scratch();

Ключевые моменты:

  • extends — ключевое слово для наследования
  • super() — вызов конструктора родительского класса
  • Дочерний класс может переопределять методы родителя (полиморфизм)

Полиморфизм

Полиморфизм позволяет использовать объекты разных классов через общий интерфейс.

class Shape {
  constructor(name) {
    this.name = name;
  }

  calculateArea() {
    throw new Error('Метод calculateArea должен быть реализован');
  }

  describe() {
    console.log(`Это ${this.name} с площадью ${this.calculateArea()}`);
  }
}

class Circle extends Shape {
  constructor(radius) {
    super('Круг');
    this.radius = radius;
  }

  calculateArea() {
    return Math.PI * this.radius ** 2;
  }
}

class Square extends Shape {
  constructor(side) {
    super('Квадрат');
    this.side = side;
  }

  calculateArea() {
    return this.side ** 2;
  }
}

class Triangle extends Shape {
  constructor(base, height) {
    super('Треугольник');
    this.base = base;
    this.height = height;
  }

  calculateArea() {
    return (this.base * this.height) / 2;
  }
}

const shapes = [
  new Circle(5),
  new Square(4),
  new Triangle(6, 3)
];

shapes.forEach(shape => {
  shape.describe();
});

Каждая фигура реализует метод calculateArea() по-своему, но все они могут быть использованы через общий интерфейс.

Статические методы и свойства

Статические методы и свойства принадлежат классу, а не его экземплярам.

class MathHelper {
  static PI = 3.14159;

  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }

  static circleArea(radius) {
    return this.PI * radius ** 2;
  }
}

console.log(MathHelper.add(5, 3));
console.log(MathHelper.circleArea(10));
console.log(MathHelper.PI);

Фабричные методы

class User {
  constructor(name, email, role) {
    this.name = name;
    this.email = email;
    this.role = role;
  }

  static createAdmin(name, email) {
    return new User(name, email, 'admin');
  }

  static createGuest(name) {
    return new User(name, 'guest@example.com', 'guest');
  }

  static createModerator(name, email) {
    return new User(name, email, 'moderator');
  }

  getInfo() {
    return `${this.name} (${this.role}) - ${this.email}`;
  }
}

const admin = User.createAdmin('Иван', 'ivan@example.com');
const guest = User.createGuest('Гость');
const moderator = User.createModerator('Мария', 'maria@example.com');

console.log(admin.getInfo());
console.log(guest.getInfo());
console.log(moderator.getInfo());

Абстракция

Абстракция позволяет выделить главное и скрыть детали реализации.

class Database {
  connect() {
    throw new Error('Метод connect должен быть реализован');
  }

  disconnect() {
    throw new Error('Метод disconnect должен быть реализован');
  }

  query(sql) {
    throw new Error('Метод query должен быть реализован');
  }
}

class MySQLDatabase extends Database {
  connect() {
    console.log('Подключение к MySQL');
  }

  disconnect() {
    console.log('Отключение от MySQL');
  }

  query(sql) {
    console.log(`Выполнение MySQL запроса: ${sql}`);
    return [];
  }
}

class PostgreSQLDatabase extends Database {
  connect() {
    console.log('Подключение к PostgreSQL');
  }

  disconnect() {
    console.log('Отключение от PostgreSQL');
  }

  query(sql) {
    console.log(`Выполнение PostgreSQL запроса: ${sql}`);
    return [];
  }
}

function executeQuery(database, sql) {
  database.connect();
  const result = database.query(sql);
  database.disconnect();
  return result;
}

const mysql = new MySQLDatabase();
const postgres = new PostgreSQLDatabase();

executeQuery(mysql, 'SELECT * FROM users');
executeQuery(postgres, 'SELECT * FROM products');

Композиция vs Наследование

Композиция — альтернатива наследованию, где объект содержит другие объекты вместо наследования от них.

class Engine {
  start() {
    console.log('Двигатель запущен');
  }

  stop() {
    console.log('Двигатель остановлен');
  }
}

class GPS {
  getLocation() {
    return { lat: 55.7558, lon: 37.6173 };
  }
}

class Radio {
  play(station) {
    console.log(`Играет радио: ${station}`);
  }
}

class Car {
  constructor() {
    this.engine = new Engine();
    this.gps = new GPS();
    this.radio = new Radio();
  }

  start() {
    this.engine.start();
    console.log('Автомобиль готов к поездке');
  }

  navigate() {
    const location = this.gps.getLocation();
    console.log(`Текущее местоположение: ${location.lat}, ${location.lon}`);
  }

  listenMusic(station) {
    this.radio.play(station);
  }
}

const car = new Car();
car.start();
car.navigate();
car.listenMusic('Rock FM');

Преимущества композиции:

  • Более гибкая структура
  • Избежание глубоких иерархий наследования
  • Легче тестировать и изменять

Прототипное ООП (до ES6)

До появления классов ООП в JavaScript реализовывалось через функции-конструкторы и прототипы.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Привет, я ${this.name}`);
};

Person.prototype.celebrateBirthday = function() {
  this.age++;
  console.log(`Теперь мне ${this.age} лет`);
};

function Developer(name, age, language) {
  Person.call(this, name, age);
  this.language = language;
}

Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;

Developer.prototype.code = function() {
  console.log(`${this.name} пишет код на ${this.language}`);
};

const dev = new Developer('Анна', 28, 'JavaScript');
dev.greet();
dev.code();
dev.celebrateBirthday();

Важно:

Современный синтаксис классов (ES6+) является синтаксическим сахаром над прототипным наследованием. Под капотом JavaScript по-прежнему использует прототипы.

Паттерны ООП в JavaScript

Singleton (Одиночка)

class Singleton {
  static instance = null;

  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
    this.data = [];
  }

  addData(item) {
    this.data.push(item);
  }

  getData() {
    return this.data;
  }
}

const instance1 = new Singleton();
instance1.addData('Первый элемент');

const instance2 = new Singleton();
instance2.addData('Второй элемент');

console.log(instance1 === instance2);
console.log(instance1.getData());

Factory (Фабрика)

class Button {
  constructor(text) {
    this.text = text;
  }

  render() {
    console.log(`Кнопка: ${this.text}`);
  }
}

class Input {
  constructor(placeholder) {
    this.placeholder = placeholder;
  }

  render() {
    console.log(`Поле ввода: ${this.placeholder}`);
  }
}

class Select {
  constructor(options) {
    this.options = options;
  }

  render() {
    console.log(`Выпадающий список: ${this.options.join(', ')}`);
  }
}

class FormElementFactory {
  static createElement(type, config) {
    switch (type) {
      case 'button':
        return new Button(config.text);
      case 'input':
        return new Input(config.placeholder);
      case 'select':
        return new Select(config.options);
      default:
        throw new Error('Неизвестный тип элемента');
    }
  }
}

const button = FormElementFactory.createElement('button', { text: 'Отправить' });
const input = FormElementFactory.createElement('input', { placeholder: 'Введите имя' });
const select = FormElementFactory.createElement('select', { options: ['Опция 1', 'Опция 2'] });

button.render();
input.render();
select.render();

Преимущества ООП

  1. Модульность
    Код разбивается на независимые объекты, каждый из которых отвечает за свою функциональность.

  2. Переиспользование
    Классы и объекты можно использовать повторно в разных частях приложения.

  3. Расширяемость
    Легко добавлять новую функциональность через наследование или композицию.

  4. Поддерживаемость
    Изменения в одном классе не влияют на другие части системы (при правильной архитектуре).

  5. Абстракция
    Скрытие сложности и предоставление простого интерфейса для взаимодействия.

Недостатки ООП

  1. Сложность
    Для небольших задач ООП может быть избыточным.

  2. Производительность
    Создание множества объектов может влиять на производительность.

  3. Глубокие иерархии
    Чрезмерное использование наследования приводит к сложным и хрупким структурам.

  4. Излишняя абстракция
    Слишком много уровней абстракции усложняют понимание кода.

Когда использовать ООП

Подходит для:

  • Больших приложений с множеством взаимосвязанных сущностей
  • Проектов, где нужна четкая структура и иерархия
  • Систем, требующих переиспользования кода
  • Командной разработки с четким разделением ответственности

Не подходит для:

  • Простых скриптов и утилит
  • Функционального программирования
  • Случаев, где композиция функций эффективнее

Итог:

ООП в JavaScript — мощная парадигма для структурирования кода. Современный синтаксис классов делает код более читаемым, но важно помнить, что под капотом JavaScript использует прототипное наследование. Выбирайте ООП, когда нужна четкая структура, инкапсуляция и переиспользование кода. Для более гибких решений рассмотрите композицию или функциональное программирование.

Связанные статьи