Классы и конструкторы в JavaScript – полное руководство с примерами
#Основы JavaScript #Объекты и прототипы #Классы (ES6)Для кого эта статья:
- Разработчики, изучающие JavaScript и его объектно-ориентированные возможности
- Профессиональные программисты, стремящиеся улучшить качество и структуру своего кода
- Студенты и начинающие разработчики, интересующиеся современными подходами к программированию в JavaScript
Классы и конструкторы в JavaScript — это не просто синтаксический сахар для прототипного наследования, а мощный инструмент структурирования кода, делающий его понятнее и поддерживаемее. За 10 лет работы с JS я видел, как разработчики превращали спагетти-код в элегантные ООП-решения благодаря грамотному использованию классов. Неважно, создаёте вы небольшие интерактивные компоненты или архитектуру корпоративного приложения — понимание классов и конструкторов критически важно для написания профессионального JavaScript-кода. Давайте разберёмся, как использовать эту мощь по максимуму. 🚀
Классы в JavaScript: основы и синтаксис
JavaScript классы, введённые в ES6 (ECMAScript 2015), предоставляют синтаксис, знакомый разработчикам, работавшим с другими объектно-ориентированными языками. Однако под капотом они всё ещё используют прототипное наследование — фундаментальный механизм JavaScript.
Базовый синтаксис объявления класса выглядит следующим образом:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
return `Привет, меня зовут ${this.name}!`;
}
}
const john = new Person('Джон', 30);
console.log(john.sayHello()); // "Привет, меня зовут Джон!"
Ключевые компоненты класса в JavaScript:
- constructor — специальный метод для создания и инициализации объектов
- this — ссылка на текущий экземпляр класса
- методы — функции, определённые внутри класса и доступные для его экземпляров
- new — оператор, используемый для создания экземпляров класса
Алексей Соколов, ведущий frontend-разработчик
Однажды наша команда унаследовала проект с 5000+ строк jQuery-кода, где одни и те же функции копировались десятки раз с минимальными изменениями. Типичный паттерн был таким: создаётся jQuery-объект, к нему привязываются методы и данные через $.data().
Первым делом мы провели рефакторинг, выделив компоненты интерфейса в классы. Например, все модальные окна стали экземплярами класса Modal с различными настройками. Код сократился на 40%, а багов при поддержке стало на 70% меньше.
Самым сложным оказалось убедить команду перейти на классы — многие считали их "излишним усложнением". Но когда стало понятно, как легко добавлять новые модальные окна, просто создавая экземпляры класса с нужными параметрами, все сомнения исчезли. Теперь мы используем классы во всех проектах как стандарт.
Важно понимать, что класс — это не просто "синтаксический сахар". Классы в JavaScript имеют несколько особенностей:
- Код внутри класса всегда выполняется в строгом режиме ('use strict')
- Методы класса неперечисляемы (non-enumerable)
- Вызов класса без new приводит к ошибке
- Переопределение имени класса внутри его тела приводит к ошибке
Вот сравнение синтаксиса классов с функциональным конструктором, который использовался до ES6:
| Особенность | Классы (ES6+) | Функциональные конструкторы (до ES6) |
|---|---|---|
| Определение | class Person { ... } | function Person() { ... } |
| Методы | Определяются в теле класса | Добавляются к prototype |
| Hoisting | Нет (temporal dead zone) | Да |
| Строгий режим | Всегда включен | По выбору |
| Использование без new | TypeError | Создаёт глобальные переменные |
Классы в JavaScript можно объявлять двумя способами:
// Объявление класса
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
// Выражение класса (анонимное)
const Square = class {
constructor(side) {
this.side = side;
}
};
// Выражение класса (именованное)
const Circle = class Circle {
constructor(radius) {
this.radius = radius;
}
};

Конструкторы и создание объектов в JS
Конструктор — это специальный метод класса, который автоматически вызывается при создании нового экземпляра. Его основная задача — инициализировать объект. В JavaScript класс может иметь только один конструктор. 🛠️
class Vehicle {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.isRunning = false;
}
startEngine() {
this.isRunning = true;
return `${this.make} ${this.model} engine started`;
}
stopEngine() {
this.isRunning = false;
return `${this.make} ${this.model} engine stopped`;
}
}
const myCar = new Vehicle('Toyota', 'Corolla', 2020);
console.log(myCar.startEngine()); // "Toyota Corolla engine started"
При создании объекта с помощью оператора new происходит следующее:
- Создаётся новый пустой объект
- Этот объект привязывается к
thisвнутри конструктора - Объект наследует свойство
prototypeкласса - Выполняется код конструктора
- Если конструктор не возвращает явно другой объект, возвращается созданный экземпляр
Существует несколько паттернов создания объектов в JavaScript:
| Паттерн | Описание | Применение |
|---|---|---|
| Конструктор класса | Использует синтаксис ES6 классов | Современные приложения, требующие ООП-подхода |
| Функциональный конструктор | Использует функцию с new | Обратная совместимость, устаревший код |
| Factory pattern | Функция создаёт и возвращает объект | Когда нужна дополнительная логика при создании |
| Object.create() | Создаёт объект с указанным прототипом | Тонкий контроль над прототипным наследованием |
Конструкторы могут быть параметризованными, что позволяет задавать значения по умолчанию:
class User {
constructor(name = 'Гость', role = 'viewer', isActive = false) {
this.name = name;
this.role = role;
this.isActive = isActive;
this.createdAt = new Date();
}
activate() {
this.isActive = true;
return `Пользователь ${this.name} активирован`;
}
}
const admin = new User('Админ', 'admin', true);
const guest = new User(); // использует значения по умолчанию
Важно отметить, что если класс расширяет другой класс и у него есть конструктор, он обязан вызвать super() перед использованием this. Мы рассмотрим это в следующем разделе о наследовании.
Наследование классов в JavaScript с extends и super
Наследование — фундаментальный принцип ООП, который позволяет создавать новые классы на основе существующих. В JavaScript для реализации наследования используются ключевые слова extends и super.
// Базовый класс
class Animal {
constructor(name) {
this.name = name;
this.speed = 0;
}
run(speed) {
this.speed = speed;
return `${this.name} бежит со скоростью ${this.speed} км/ч`;
}
stop() {
this.speed = 0;
return `${this.name} стоит неподвижно`;
}
}
// Класс-наследник
class Rabbit extends Animal {
constructor(name, earLength) {
super(name); // Вызываем конструктор родительского класса
this.earLength = earLength;
}
hide() {
return `${this.name} прячется`;
}
// Переопределение метода родителя
stop() {
// Вызываем родительский метод
let parentResult = super.stop();
return `${parentResult} и прижимает уши`;
}
}
const rabbit = new Rabbit('Белый кролик', 10);
console.log(rabbit.run(5)); // "Белый кролик бежит со скоростью 5 км/ч"
console.log(rabbit.hide()); // "Белый кролик прячется"
console.log(rabbit.stop()); // "Белый кролик стоит неподвижно и прижимает уши"
Ключевые особенности наследования в JavaScript:
- extends — указывает, от какого класса наследовать
- super() в конструкторе — вызывает родительский конструктор
- super.method() — вызывает родительский метод
- Доступ к родительским свойствам и методам
- Возможность переопределения (override) методов родителя
Важные правила использования super:
- В конструкторе дочернего класса
super()должен быть вызван до использованияthis - Если дочерний класс не имеет своего конструктора, то конструктор родителя вызывается автоматически с переданными аргументами
- Вызов
super.method()ищет метод в прототипе родительского класса
Михаил Петров, архитектор программного обеспечения
В проекте электронной коммерции мы столкнулись с проблемой дублирования кода при создании различных типов товаров. У нас были физические товары, цифровые загрузки, подписки, и все они имели схожую, но не идентичную логику.
Решение пришло через иерархию классов. Мы создали базовый класс Product с общими свойствами и методами:
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
this.createdAt = new Date();
}
getDisplayPrice() {
return `$${this.price.toFixed(2)}`;
}
calculateTax(rate = 0.1) {
return this.price * rate;
}
}
Затем мы расширили этот класс для конкретных типов товаров:
class PhysicalProduct extends Product {
constructor(id, name, price, weight, dimensions) {
super(id, name, price);
this.weight = weight;
this.dimensions = dimensions;
this.requiresShipping = true;
}
calculateShippingCost(baseRate = 5) {
return baseRate + (this.weight * 0.1);
}
}
class DigitalProduct extends Product {
constructor(id, name, price, downloadSize, format) {
super(id, name, price);
this.downloadSize = downloadSize;
this.format = format;
this.requiresShipping = false;
}
calculateTax(rate = 0.05) {
// Переопределяем для другой ставки налога
return super.calculateTax(rate);
}
generateDownloadLink() {
return `https://downloads.example.com/${this.id}`;
}
}
Это решение снизило количество кода на 35% и сделало систему гораздо более поддерживаемой. Когда потребовалось добавить новый тип товаров (подписки), мы просто создали новый дочерний класс, не нарушая существующую логику.
JavaScript поддерживает цепочки наследования любой длины, но слишком глубокие иерархии могут усложнить понимание кода. Как правило, рекомендуется ограничивать глубину наследования 2-3 уровнями.
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
this.isRunning = false;
}
startEngine() { /* ... */ }
stopEngine() { /* ... */ }
}
class Car extends Vehicle {
constructor(make, model, doors) {
super(make, model);
this.doors = doors;
this.type = 'car';
}
drift() { /* ... */ }
}
class ElectricCar extends Car {
constructor(make, model, doors, batteryCapacity) {
super(make, model, doors);
this.batteryCapacity = batteryCapacity;
this.fuelType = 'electricity';
}
charge() { /* ... */ }
}
Работа с методами и свойствами в классах JavaScript
JavaScript классы предоставляют разнообразные способы работы с методами и свойствами, что позволяет создавать гибкий и выразительный код. 💡
Рассмотрим основные типы методов и свойств в классах:
class Product {
// Публичные свойства (с ES2022)
name;
price;
// Приватное свойство (с ES2022)
#inventory = 0;
// Статическое свойство
static taxRate = 0.1;
constructor(name, price, inventory) {
this.name = name;
this.price = price;
this.#inventory = inventory;
}
// Публичный метод
display() {
return `${this.name}: $${this.price}`;
}
// Приватный метод (с ES2022)
#validateInventory() {
return this.#inventory > 0;
}
// Метод с использованием приватных свойств и методов
canPurchase(quantity = 1) {
return this.#validateInventory() && this.#inventory >= quantity;
}
// Геттер
get inStock() {
return this.#inventory > 0;
}
// Сеттер
set inventory(value) {
if (value < 0) {
throw new Error("Inventory cannot be negative");
}
this.#inventory = value;
}
// Статический метод
static calculateTax(price) {
return price * Product.taxRate;
}
}
const laptop = new Product('Laptop', 999, 5);
console.log(laptop.display()); // "Laptop: $999"
console.log(laptop.inStock); // true
console.log(Product.calculateTax(laptop.price)); // 99.9
Основные виды методов и свойств в JavaScript классах:
| Тип | Назначение | Синтаксис |
|---|---|---|
| Публичные методы | Доступны всем экземплярам и внешнему коду | methodName() {} |
| Публичные свойства | Данные, доступные всем | propertyName; или через this |
| Приватные методы | Внутренняя логика, скрытая от внешнего мира | #methodName() {} |
| Приватные свойства | Данные, доступные только внутри класса | #propertyName; |
| Геттеры | Получение данных в виде свойства | get propertyName() {} |
| Сеттеры | Установка данных с валидацией | set propertyName(value) {} |
| Статические методы | Относятся к классу, не к экземплярам | static methodName() {} |
| Статические свойства | Данные, общие для всего класса | static propertyName = value; |
Геттеры и сеттеры особенно полезны для создания вычисляемых свойств и обеспечения инкапсуляции:
class Circle {
#radius;
constructor(radius) {
this.radius = radius; // Вызовет сеттер
}
// Геттер для получения радиуса
get radius() {
return this.#radius;
}
// Сеттер с валидацией
set radius(value) {
if (value <= 0) {
throw new Error('Радиус должен быть положительным числом');
}
this.#radius = value;
}
// Вычисляемое свойство
get area() {
return Math.PI * this.#radius ** 2;
}
// Вычисляемое свойство
get circumference() {
return 2 * Math.PI * this.#radius;
}
}
const circle = new Circle(5);
console.log(circle.radius); // 5
console.log(circle.area); // ~78.54
circle.radius = 10;
console.log(circle.area); // ~314.16
Статические методы и свойства принадлежат классу, а не его экземплярам, и часто используются для создания утилит и фабричных методов:
class MathUtils {
static PI = 3.14159;
static sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
static average(...numbers) {
return MathUtils.sum(...numbers) / numbers.length;
}
// Фабричный метод
static createPoint(x, y) {
return { x, y };
}
}
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.sum(1, 2, 3, 4)); // 10
console.log(MathUtils.average(1, 2, 3, 4)); // 2.5
const point = MathUtils.createPoint(10, 20);
При работе с методами и свойствами стоит помнить о некоторых лучших практиках:
- Используйте приватные свойства (#) для внутренних данных, которые не должны быть доступны извне
- Предпочитайте геттеры и сеттеры для контроля доступа к данным
- Статические методы лучше применять для операций, которые не зависят от состояния экземпляра
- Избегайте избыточных методов, нарушающих принцип единственной ответственности
Продвинутые возможности классов и практические кейсы
Классы в JavaScript предоставляют ряд продвинутых возможностей, которые делают их мощным инструментом для решения сложных задач разработки. Рассмотрим некоторые из них. 🔥
1. Композиция вместо наследования
Хотя наследование является мощным инструментом, часто предпочтительнее использовать композицию — подход, при котором объекты содержат экземпляры других классов вместо наследования их поведения:
class Engine {
constructor(type, horsepower) {
this.type = type;
this.horsepower = horsepower;
}
start() {
return `${this.type} engine started`;
}
stop() {
return `${this.type} engine stopped`;
}
}
class Transmission {
constructor(type, gears) {
this.type = type;
this.gears = gears;
this.currentGear = 0;
}
shiftUp() {
if (this.currentGear < this.gears) {
this.currentGear++;
return `Shifted to gear ${this.currentGear}`;
}
return `Already in highest gear`;
}
shiftDown() {
if (this.currentGear > 0) {
this.currentGear--;
return `Shifted to gear ${this.currentGear === 0 ? 'neutral' : this.currentGear}`;
}
return `Already in neutral`;
}
}
// Используем композицию
class Car {
constructor(make, model, engineType, engineHP, transType, gears) {
this.make = make;
this.model = model;
this.engine = new Engine(engineType, engineHP);
this.transmission = new Transmission(transType, gears);
}
start() {
return this.engine.start();
}
stop() {
return this.engine.stop();
}
shiftUp() {
return this.transmission.shiftUp();
}
shiftDown() {
return this.transmission.shiftDown();
}
getInfo() {
return `${this.make} ${this.model} with ${this.engine.horsepower}hp ${this.engine.type} engine and ${this.transmission.type} ${this.transmission.gears}-speed transmission`;
}
}
const myCar = new Car('Honda', 'Accord', 'V6', 278, 'automatic', 6);
console.log(myCar.getInfo());
console.log(myCar.start());
console.log(myCar.shiftUp());
2. Миксины для множественного наследования
JavaScript не поддерживает множественное наследование напрямую, но можно использовать миксины — объекты, методы которых "примешиваются" к прототипу класса:
// Миксин с методами для работы с событиями
const EventMixin = {
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
off(eventName, handler) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) return;
this._eventHandlers[eventName] = this._eventHandlers[eventName].filter(h => h !== handler);
},
trigger(eventName, ...args) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) return;
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
// Миксин с методами для сериализации
const SerializableMixin = {
toJSON() {
const json = {};
Object.keys(this).forEach(key => {
if (key[0] !== '_') { // Не сериализуем приватные свойства
json[key] = this[key];
}
});
return json;
},
fromJSON(json) {
Object.keys(json).forEach(key => {
this[key] = json[key];
});
}
};
// Класс, использующий оба миксина
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// Применяем миксины
Object.assign(User.prototype, EventMixin, SerializableMixin);
const user = new User("John", 30);
// Используем функциональность из EventMixin
user.on('update', () => console.log('User updated!'));
user.trigger('update');
// Используем функциональность из SerializableMixin
const json = user.toJSON();
console.log(json); // {name: "John", age: 30}
const newUser = new User("", 0);
newUser.fromJSON(json);
console.log(newUser.name); // "John"
3. Абстрактные классы
JavaScript не имеет встроенной поддержки абстрактных классов, но мы можем эмулировать это поведение:
// Абстрактный класс Shape
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Cannot instantiate abstract class");
}
}
// Абстрактный метод
calculateArea() {
throw new Error("Method 'calculateArea' must be implemented");
}
// Абстрактный метод
calculatePerimeter() {
throw new Error("Method 'calculatePerimeter' must be implemented");
}
// Обычный метод
toString() {
return `Shape with area: ${this.calculateArea()} and perimeter: ${this.calculatePerimeter()}`;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
calculatePerimeter() {
return 2 * (this.width + this.height);
}
}
// const shape = new Shape(); // Error: Cannot instantiate abstract class
const rectangle = new Rectangle(5, 10);
console.log(rectangle.toString()); // "Shape with area: 50 and perimeter: 30"
4. Шаблоны проектирования с использованием классов
Классы JavaScript отлично подходят для реализации популярных шаблонов проектирования:
- Singleton (Одиночка) — гарантирует, что класс имеет только один экземпляр
- Factory (Фабрика) — создаёт объекты без указания конкретного класса
- Observer (Наблюдатель) — определяет зависимость "один ко многим"
- Decorator (Декоратор) — динамически добавляет ответственность объекту
Пример реализации паттерна Singleton:
class Database {
constructor(host, username, password) {
if (Database.instance) {
return Database.instance;
}
this.host = host;
this.username = username;
this.password = password;
this.connected = false;
// Сохраняем экземпляр
Database.instance = this;
}
connect() {
if (this.connected) {
return "Already connected";
}
// Имитация подключения к БД
console.log(`Connecting to ${this.host} as ${this.username}...`);
this.connected = true;
return "Connection established";
}
query(sql) {
if (!this.connected) {
return "Not connected to database";
}
console.log(`Executing: ${sql}`);
return `Results for ${sql}`;
}
}
const db1 = new Database('localhost', 'admin', 'secret');
console.log(db1.connect());
const db2 = new Database('another-host', 'root', 'password');
console.log(db1 === db2); // true – это тот же экземпляр
console.log(db2.query('SELECT * FROM users'));
5. Реактивные классы с использованием Proxy
Можно создавать реактивные классы, которые автоматически реагируют на изменения своих свойств с помощью Proxy:
class ReactiveModel {
constructor(data = {}) {
this._data = data;
this._subscribers = [];
return new Proxy(this, {
set(target, property, value) {
// Игнорируем приватные свойства
if (property.startsWith('_')) {
target[property] = value;
return true;
}
const oldValue = target._data[property];
target._data[property] = value;
// Уведомляем подписчиков об изменениях
target._notifySubscribers(property, oldValue, value);
return true;
},
get(target, property) {
// Для методов возвращаем их как есть
if (typeof target[property] === 'function') {
return target[property].bind(target);
}
// Игнорируем приватные свойства
if (property.startsWith('_')) {
return target[property];
}
return target._data[property];
}
});
}
subscribe(callback) {
this._subscribers.push(callback);
return () => {
this._subscribers = this._subscribers.filter(cb => cb !== callback);
};
}
_notifySubscribers(property, oldValue, newValue) {
this._subscribers.forEach(callback => {
callback({
property,
oldValue,
newValue
});
});
}
}
const user = new ReactiveModel({ name: 'John', age: 30 });
// Подписываемся на изменения
const unsubscribe = user.subscribe(({ property, oldValue, newValue }) => {
console.log(`Property "${property}" changed from "${oldValue}" to "${newValue}"`);
});
user.name = 'Jane'; // Property "name" changed from "John" to "Jane"
user.age = 31; // Property "age" changed from "30" to "31"
// Отписываемся
unsubscribe();
user.name = 'Alice'; // Никаких уведомлений, т.к. мы отписались
Эти продвинутые техники демонстрируют гибкость и мощь классов в JavaScript, позволяя создавать сложные архитектуры и решать широкий спектр задач.
Объектно-ориентированное программирование в JavaScript прошло долгий путь от прототипного наследования до современных классов. Понимая нюансы работы классов и конструкторов, вы получаете мощный инструмент для создания масштабируемой и поддерживаемой архитектуры приложений. Помните: класс — это не только способ организации кода, но и выражение намерений разработчика о том, как данные и поведение должны взаимодействовать. Используйте классы осознанно, предпочитая композицию наследованию, когда это возможно, и ваш код станет более понятным, гибким и устойчивым к изменениям.
Читайте также
Кристина Крылова
JavaScript-инженер