ООП: Инкапсуляция и полиморфизм в JavaScript (Node.js): подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы — шеф-повар в ресторане. У вас есть холодильник, плита, ножи и продукты. Чтобы приготовить блюдо, вы открываете холодильник, достаёте продукты, режете их, жарите на плите. Всё это — внутренняя кухня, которую посетители ресторана не видят. Они видят только готовое блюдо на тарелке.
Теперь представьте, что вы — посетитель ресторана. Вы не заходите на кухню, не трогаете ножи и не включаете плиту. Вы просто делаете заказ официанту: «Принесите, пожалуйста, пасту карбонара». Через некоторое время вам приносят готовое блюдо. Вы не знаете, как именно его готовили — это скрыто от вас. Вы только взаимодействуете с результатом.
В программировании происходит то же самое. У нас есть объекты (как шеф-повар), у которых есть внутренние данные (продукты, рецепты) и методы (нарезка, жарка). Но мы, как программисты, не хотим, чтобы кто-то посторонний мог залезть внутрь объекта и испортить его данные. Мы хотим, чтобы с объектом можно было общаться только через специальные публичные методы (как через официанта).
Это и есть инкапсуляция — сокрытие внутреннего устройства объекта и предоставление только безопасного интерфейса для работы с ним.
А что такое полиморфизм? Представьте, что у вас есть универсальный пульт управления. На нём есть кнопка «Воспроизвести». Если вы нажмёте её на телевизоре — включится фильм. Если нажмёте на музыкальном центре — заиграет песня. Если нажмёте на игровой приставке — запустится игра. Одна и та же команда («Воспроизвести») работает по-разному в зависимости от того, к какому устройству она применяется.
В программировании полиморфизм — это способность разных объектов по-разному реагировать на одну и ту же команду. Например, у нас есть класс «Кошка» и класс «Собака». У обоих есть метод «податьГолос()». Но кошка скажет «Мяу», а собака — «Гав». Одна команда — разное поведение.
💡 Запомните главное: Инкапсуляция — это про «что можно трогать, а что нельзя». Полиморфизм — это про «один интерфейс — много реализаций».
Теория: как это работает изнутри
Инкапсуляция: защита данных
В JavaScript инкапсуляция реализуется через замыкания и приватные поля (начиная с ES2022). Давайте разберём, что это значит.
Когда вы создаёте объект, у него есть свойства (данные) и методы (функции). Без инкапсуляции любое свойство можно изменить напрямую:
// ПЛОХОЙ ПРИМЕР — без инкапсуляции
let пользователь = {
имя: "Иван",
пароль: "12345" // Любой может это прочитать и изменить!
};
// Любой может сделать так:
пользователь.пароль = "взломан"; // Опасность!
console.log(пользователь.пароль); // "взломан"
С инкапсуляцией мы прячем пароль и даём доступ только через специальные методы:
// ХОРОШИЙ ПРИМЕР — с инкапсуляцией
function СоздатьПользователя(имя, пароль) {
// Приватная переменная — её нельзя получить снаружи
let _пароль = пароль;
return {
имя: имя,
// Публичный метод для проверки пароля
проверитьПароль(введенныйПароль) {
return _пароль === введенныйПароль;
},
// Публичный метод для смены пароля
сменитьПароль(старыйПароль, новыйПароль) {
if (_пароль === старыйПароль) {
_пароль = новыйПароль;
return true;
}
return false;
}
};
}
let пользователь = СоздатьПользователя("Иван", "12345");
console.log(пользователь._пароль); // undefined — не видно!
console.log(пользователь.проверитьПароль("12345")); // true
пользователь.сменитьПароль("12345", "новый_пароль"); // успешно
Полиморфизм: один интерфейс — много реализаций
Полиморфизм в JavaScript достигается через наследование и переопределение методов. Когда дочерний класс наследует метод от родительского, он может переопределить его — то есть написать свою версию.
Представьте, что у нас есть базовый класс «Транспорт» с методом «двигаться()». Автомобиль будет ехать по дороге, лодка — плыть по воде, самолёт — лететь по воздуху. Все они «двигаются», но по-разному.
// Базовый класс
class Транспорт {
двигаться() {
console.log("Транспорт движется");
}
}
// Дочерние классы переопределяют метод
class Автомобиль extends Транспорт {
двигаться() {
console.log("🚗 Едет по дороге");
}
}
class Лодка extends Транспорт {
двигаться() {
console.log("🚤 Плывёт по воде");
}
}
class Самолет extends Транспорт {
двигаться() {
console.log("✈️ Летит по воздуху");
}
}
// Полиморфизм в действии:
function запуститьДвижение(транспорт) {
транспорт.двигаться(); // Одна команда — разное поведение
}
запуститьДвижение(new Автомобиль()); // 🚗 Едет по дороге
запуститьДвижение(new Лодка()); // 🚤 Плывёт по воде
запуститьДвижение(new Самолет()); // ✈️ Летит по воздуху
🔑 Ключевая идея: ФункциязапуститьДвижениене знает, с каким именно транспортом она работает. Она просто вызывает методдвигаться(), и каждый объект сам решает, как его выполнить.
Синтаксис в JavaScript (Node.js): все формы и варианты
1. Инкапсуляция через замыкания (старый способ, до ES6)
// Фабричная функция с замыканием
function Счет(начальнаяСумма) {
// Приватная переменная — доступна только внутри функции
let _баланс = начальнаяСумма;
// Возвращаем объект с методами
return {
// Геттер — получает значение
получитьБаланс() {
return _баланс;
},
// Сеттер — изменяет значение с проверкой
пополнить(сумма) {
if (сумма > 0) {
_баланс += сумма;
return true;
}
return false;
},
снять(сумма) {
if (сумма > 0 && сумма <= _баланс) {
_баланс -= сумма;
return true;
}
return false;
}
};
}
let мойСчет = Счет(1000);
console.log(мойСчет.получитьБаланс()); // 1000
мойСчет.пополнить(500);
console.log(мойСчет.получитьБаланс()); // 1500
// мойСчет._баланс — undefined, не видно!
2. Инкапсуляция через приватные поля (современный способ, ES2022+)
// Используем символ # для обозначения приватных полей
class БанковскийСчет {
// Приватное поле — начинается с #
#баланс;
#владелец;
constructor(владелец, начальныйБаланс = 0) {
this.#владелец = владелец;
this.#баланс = начальныйБаланс;
}
// Публичный метод
получитьБаланс() {
return this.#баланс;
}
получитьВладельца() {
return this.#владелец;
}
пополнить(сумма) {
if (сумма > 0) {
this.#баланс += сумма;
console.log(`Счёт пополнен на ${сумма}. Баланс: ${this.#баланс}`);
}
}
снять(сумма) {
if (сумма > 0 && сумма <= this.#баланс) {
this.#баланс -= сумма;
console.log(`Снято ${сумма}. Баланс: ${this.#баланс}`);
return true;
}
console.log("Недостаточно средств");
return false;
}
}
let счет = new БанковскийСчет("Иван", 1000);
// счет.#баланс — SyntaxError! Нельзя получить доступ извне
console.log(счет.получитьБаланс()); // 1000
3. Полиморфизм через наследование классов
// Базовый класс
class Фигура {
constructor(название) {
this.название = название;
}
// Метод, который будут переопределять
площадь() {
return 0;
}
описание() {
return `${this.название} имеет площадь ${this.площадь()}`;
}
}
// Дочерний класс — Прямоугольник
class Прямоугольник extends Фигура {
constructor(ширина, высота) {
super("Прямоугольник"); // Вызываем конструктор родителя
this.ширина = ширина;
this.высота = высота;
}
// Переопределяем метод площадь()
площадь() {
return this.ширина * this.высота;
}
}
// Дочерний класс — Круг
class Круг extends Фигура {
constructor(радиус) {
super("Круг");
this.радиус = радиус;
}
площадь() {
return Math.PI * this.радиус ** 2;
}
}
// Дочерний класс — Треугольник
class Треугольник extends Фигура {
constructor(основание, высота) {
super("Треугольник");
this.основание = основание;
this.высота = высота;
}
площадь() {
return (this.основание * this.высота) / 2;
}
}
// Полиморфная функция
function показатьПлощадь(фигура) {
console.log(`${фигура.название}: ${фигура.площадь().toFixed(2)} кв.ед.`);
}
показатьПлощадь(new Прямоугольник(5, 10)); // Прямоугольник: 50.00 кв.ед.
показатьПлощадь(new Круг(7)); // Круг: 153.94 кв.ед.
показатьПлощадь(new Треугольник(6, 8)); // Треугольник: 24.00 кв.ед.
4. Полиморфизм через утиную типизацию (Duck Typing)
// В JavaScript не нужно явно указывать тип —
// если объект имеет нужный метод, он подходит
const утка = {
имя: "Утка",
говорить() {
return "Кря-кря!";
}
};
const собака = {
имя: "Собака",
говорить() {
return "Гав-гав!";
}
};
const кошка = {
имя: "Кошка",
говорить() {
return "Мяу-мяу!";
}
};
// Функция принимает любой объект с методом говорить()
function заставитьГоворить(животное) {
console.log(`${животное.имя} говорит: ${животное.говорить()}`);
}
заставитьГоворить(утка); // Утка говорит: Кря-кря!
заставитьГоворить(собака); // Собака говорит: Гав-гав!
заставитьГоворить(кошка); // Кошка говорит: Мяу-мяу!
Примеры кода — от простого к сложному
Пример 1: Простейшая инкапсуляция — счётчик
// Простейший пример: счётчик с защищённым значением
class Счетчик {
#значение = 0; // Приватное поле
увеличить() {
this.#значение++;
}
получитьЗначение() {
return this.#значение;
}
сбросить() {
this.#значение = 0;
}
}
const счетчик = new Счетчик();
счетчик.увеличить();
счетчик.увеличить();
счетчик.увеличить();
console.log(счетчик.получитьЗначение()); // 3
// счетчик.#значение — ошибка! Нельзя изменить напрямую
Пример 2: Инкапсуляция с валидацией данных
// Класс для хранения возраста с проверкой
class Человек {
#возраст;
constructor(имя, возраст) {
this.имя = имя;
this.#установитьВозраст(возраст); // Используем приватный метод
}
// Приватный метод для установки возраста с проверкой
#установитьВозраст(возраст) {
if (возраст >= 0 && возраст <= 150) {
this.#возраст = возраст;
} else {
this.#возраст = 0;
console.log("Некорректный возраст, установлено 0");
}
}
// Публичный метод для получения возраста
получитьВозраст() {
return this.#возраст;
}
// Публичный метод для изменения возраста
отметитьДеньРождения() {
this.#возраст++;
console.log(`С днём рождения! Теперь ${this.#возраст} лет`);
}
}
const человек = new Человек("Анна", 25);
console.log(человек.получитьВозраст()); // 25
человек.отметитьДеньРождения(); // С днём рождения! Теперь 26 лет
// человек.#возраст = 200 — ошибка! Защищено
Пример 3: Полиморфизм с животными
// Базовый класс для всех животных
class Животное {
constructor(имя) {
this.имя = имя;
}
// Базовый метод — будет переопределён
издатьЗвук() {
return "...";
}
представить() {
return `${this.имя} говорит: ${this.издатьЗвук()}`;
}
}
class Собака extends Животное {
издатьЗвук() {
return "Гав-гав!";
}
}
class Кошка extends Животное {
издатьЗвук() {
return "Мяу-мяу!";
}
}
class Корова extends Животное {
издатьЗвук() {
return "Му-у-у!";
}
}
// Функция, использующая полиморфизм
function хорЖивотных(животные) {
for (let животное of животные) {
console.log(животное.представить());
}
}
const животные = [
new Собака("Бобик"),
new Кошка("Мурка"),
new Корова("Зорька")
];
хорЖивотных(животные);
// Бобик говорит: Гав-гав!
// Мурка говорит: Мяу-мяу!
// Зорька говорит: Му-у-у!
Пример 4: Инкапсуляция + полиморфизм — система оплаты
// Базовый класс для способов оплаты
class СпособОплаты {
#название;
constructor(название) {
this.#название = название;
}
получитьНазвание() {
return this.#название;
}
// Абстрактный метод — будет переопределён
оплатить(сумма) {
throw new Error("Метод оплатить() должен быть переопределён");
}
}
class Наличные extends СпособОплаты {
constructor() {
super("Наличные");
}
оплатить(сумма) {
console.log(`💵 Оплачено наличными: ${сумма} руб.`);
return true;
}
}
class БанковскаяКарта extends СпособОплаты {
#номерКарты;
#пинКод;
constructor(номерКарты, пинКод) {
super("Банковская карта");
this.#номерКарты = номерКарты;
this.#пинКод = пинКод;
}
оплатить(сумма) {
console.log(`💳 Оплачено картой ${this.#номерКарты.slice(-4)}: ${сумма} руб.`);
return true;
}
// Приватный метод для проверки ПИН-кода
#проверитьПин(введенныйПин) {
return this.#пинКод === введенныйПин;
}
// Публичный метод с проверкой
оплатитьСПином(сумма, пин) {
if (this.#проверитьПин(пин)) {
return this.оплатить(сумма);
}
console.log("❌ Неверный ПИН-код");
return false;
}
}
class Криптовалюта extends СпособОплаты {
#кошелек;
constructor(адресКошелька) {
super("Криптовалюта");
this.#кошелек = адресКошелька;
}
оплатить(сумма) {
console.log(`₿ Оплачено криптовалютой с кошелька ${this.#кошелек.slice(0, 6)}...: ${сумма} USD`);
return true;
}
}
// Полиморфная функция для обработки платежа
function обработатьПлатеж(способ, сумма) {
console.log(`Обработка платежа через ${способ.получитьНазвание()}...`);
return способ.оплатить(сумма);
}
// Использование
const наличные = new Наличные();
const карта = new БанковскаяКарта("1234567890123456", "1234");
const крипта = new Криптовалюта("0xABC123DEF456");
обработатьПлатеж(наличные, 500); // 💵 Оплачено наличными: 500 руб.
обработатьПлатеж(карта, 1500); // 💳 Оплачено картой 3456: 1500 руб.
обработатьПлатеж(крипта, 200); // ₿ Оплачено криптовалютой...
// Проверка ПИН-кода
карта.оплатитьСПином(3000, "1234"); // 💳 Оплачено картой 3456: 3000 руб.
карта.оплатитьСПином(3000, "0000"); // ❌ Неверный ПИН-код
Пример 5: Инкапсуляция с геттерами и сеттерами
// Использование get и set для контроля доступа
class Термометр {
#градусыЦельсия;
constructor(градусыЦельсия = 0) {
this.#градусыЦельсия = градусыЦельсия;
}
// Геттер — получает значение
get цельсий() {
return this.#градусыЦельсия;
}
// Сеттер — устанавливает с проверкой
set цельсий(значение) {
if (значение < -273.15) {
console.log("Температура ниже абсолютного нуля невозможна!");
this.#градусыЦельсия = -273.15;
} else {
this.#градусыЦельсия = значение;
}
}
// Вычисляемое свойство — конвертация в Фаренгейты
get фаренгейт() {
return this.#градусыЦельсия * 9/5 + 32;
}
set фаренгейт(значение) {
this.цельсий = (значение - 32) * 5/9;
}
}
const термометр = new Термометр(25);
console.log(термометр.цельсий); // 25 (используем геттер)
console.log(термометр.фаренгейт); // 77 (вычисляется автоматически)
термометр.цельсий = 100; // Используем сеттер
console.log(термометр.фаренгейт); // 212
термометр.цельсий = -300; // Ошибка! Ниже абсолютного нуля
console.log(термометр.цельсий); // -273.15 (защита сработала)
термометр.фаренгейт = 32; // Устанавливаем через Фаренгейты
console.log(термометр.цельсий); // 0 (конвертировалось)
Пример 6: Полиморфизм с интерфейсами (через проверку методов)
// В JavaScript нет интерфейсов, но мы можем проверять наличие методов
class Птица {
constructor(имя) {
this.имя = имя;
}
летать() {
return `${this.имя} летит`;
}
}
class Самолет {
constructor(модель) {
this.модель = модель;
}
летать() {
return `Самолёт ${this.модель} летит`;
}
}
class Ракета {
constructor(название) {
this.название = название;
}
летать() {
return `Ракета ${this.название} взлетает!`;
}
}
// Функция проверяет, есть ли метод летать()
function запуститьВПолёт(объект) {
if (typeof объект.летать === 'function') {
console.log(объект.летать());
} else {
console.log("Этот объект не умеет летать!");
}
}
запуститьВПолёт(new Птица("Орёл")); // Орёл летит
запуститьВПолёт(new Самолет("Боинг")); // Самолёт Боинг летит
запуститьВПолёт(new Ракета("Союз")); // Ракета Союз взлетает!
запуститьВПолёт({имя: "Камень"}); // Этот объект не умеет летать!
Пример 7: Комплексный пример — система управления библиотекой
// Полноценный пример с инкапсуляцией и полиморфизмом
// Базовый класс для всех материалов библиотеки
class БиблиотечныйМатериал {
#id;
#название;
#годИздания;
#доступен = true;
constructor(id, название, годИздания) {
this.#id = id;
this.#название = название;
this.#годИздания = годИздания;
}
// Геттеры для доступа к приватным полям
получитьId() { return this.#id; }
получитьНазвание() { return this.#название; }
получитьГод() { return this.#годИздания; }
isДоступен() { return this.#доступен; }
// Публичные методы для управления доступностью
выдать() {
if (this.#доступен) {
this.#доступен = false;
return true;
}
return false;
}
вернуть() {
this.#доступен = true;
}
// Полиморфный метод — будет переопределён
получитьОписание() {
return `${this.#название} (${this.#годИздания})`;
}
}
// Класс для книг
class Книга extends БиблиотечныйМатериал {
#автор;
#страницы;
constructor(id, название, год, автор, страницы) {
super(id, название, год);
this.#автор = автор;
this.#страницы = страницы;
}
получитьАвтора() { return this.#автор; }
// Переопределяем метод
получитьОписание() {
return `📚 Книга: "${super.получитьНазвание()}" — ${this.#автор}, ${super.получитьГод()}г., ${this.#страницы} стр.`;
}
}
// Класс для журналов
class Журнал extends БиблиотечныйМатериал {
#номерВыпуска;
constructor(id, название, год, номерВыпуска) {
super(id, название, год);
this.#номерВыпуска = номерВыпуска;
}
получитьНомерВыпуска() { return this.#номерВыпуска; }
получитьОписание() {
return `📰 Журнал: "${super.получитьНазвание()}" №${this.#номерВыпуска}, ${super.получитьГод()}г.`;
}
}
// Класс для DVD
class DVD extends БиблиотечныйМатериал {
#режиссер;
#длительность; // в минутах
constructor(id, название, год, режиссер, длительность) {
super(id, название, год);
this.#режиссер = режиссер;
this.#длительность = длительность;
}
получитьРежиссера() { return this.#режиссер; }
получитьОписание() {
return `🎬 DVD: "${super.получитьНазвание()}" — реж. ${this.#режиссер}, ${super.получитьГод()}г., ${this.#длительность} мин.`;
}
}
// Класс для управления библиотекой
class Библиотека {
#материалы = [];
#читатели = [];
// Приватный метод для генерации ID
#сгенерироватьId() {
return this.#материалы.length + 1;
}
добавитьМатериал(материал) {
this.#материалы.push(материал);
console.log(`✅ Добавлен: ${материал.получитьОписание()}`);
}
// Полиморфная функция — работает с любым материалом
показатьВсеМатериалы() {
console.log("
📋 Каталог библиотеки:");
this.#материалы.forEach((материал, индекс) => {
const статус = материал.isДоступен() ? "✅ Доступен" : "❌ Выдан";
console.log(`${индекс + 1}. ${материал.получитьОписание()} — ${статус}`);
});
}
выдатьМатериал(id) {
const материал = this.#материалы.find(m => m.получитьId() === id);
if (материал) {
if (материал.выдать()) {
console.log(`📤 Выдан: ${материал.получитьОписание()}`);
} else {
console.log(`❌ Материал уже выдан: ${материал.получитьОписание()}`);
}
} else {
console.log("❌ Материал не найден");
}
}
вернутьМатериал(id) {
const материал = this.#материалы.find(m => m.получитьId() === id);
if (материал) {
материал.вернуть();
console.log(`📥 Возвращён: ${материал.получитьОписание()}`);
}
}
}
// Демонстрация работы
const библиотека = new Библиотека();
// Добавляем разные материалы
библиотека.добавитьМатериал(new Книга(1, "Война и мир", 1869, "Л.Н. Толстой", 1225));
библиотека.добавитьМатериал(new Журнал(2, "Наука и жизнь", 2023, 5));
библиотека.добавитьМатериал(new DVD(3, "Матрица", 1999, "Братья Вачовски", 136));
// Показываем каталог
библиотека.показатьВсеМатериалы();
// Выдаём и возвращаем
библиотека.выдатьМатериал(1);
библиотека.выдатьМатериал(1); // Попытка выдать уже выданное
библиотека.вернутьМатериал(1);
библиотека.показатьВсеМатериалы();
Сравнение с Python: чем похоже и чем отличается
| Характеристика | JavaScript (Node.js) | Python |
|---|---|---|
| Приватные поля | Используется символ # (с ES2022): #поле |
Используется двойное подчёркивание: __поле (name mangling) |
| Геттеры/сеттеры | Через ключевые слова get и set |
Через декораторы @property и @поле.setter |
| Наследование | Через extends, вызов родителя через super() |
Через class Дочерний(Родитель), вызов через super().__init__() |
| Полиморфизм | Утиная типизация (duck typing) — проверка наличия метода | Утиная типизация + абстрактные классы через ABC |
| Абстрактные классы | Нет встроенной поддержки (можно эмулировать) | Есть через модуль abc |
| Множественное наследование | Нет (только через приме |