
Привет, меня зовут Саша, и я продолжаю рассказывать о JavaScript тем, кто только взялся за освоение этого языка. В прошлой статье мы разобрали функции-конструкторы и оператор new, научившись создавать шаблоны для множества однотипных объектов.
Сегодня мы сделаем следующий шаг к современному JavaScript. Я покажу вам классы — более чистый и понятный способ делать ровно то же самое. Хоть классы и называют «синтаксическим сахаром», но это не отменяет популярность и удобство их использования. Поехали!
От конструктора к классу
Прежде чем пойдем дальше, советую глянуть прошлую статью о функциях-конструкторах и операторе new. Если читать последовательно, то будет гораздо проще понять о чем я говорю здесь.
Давайте посмотрим на пример функции-конструктора из прошлой статьи, которая создает объекты товаров. Кстати, такие созданные объекты правильно называть экземплярами. Каждый new Product(...) порождает новый экземпляр (конкретный экземпляр товара) по общему шаблону.
function Product(name, price, discountPercent) {
this.name = name;
this.price = price;
this.discountPercent = discountPercent;
this.getFinalPrice = function () {
const discountAmount = this.price * (this.discountPercent / 100);
return this.price - discountAmount;
};
this.logInfo = function () {
const finalPrice = this.getFinalPrice();
console.log(${this.name});
console.log( Цена: ${this.price} руб.);
console.log( Скидка: ${this.discountPercent}%);
console.log( Итог: ${finalPrice.toFixed(2)} руб.);
console.log("---");
};
}
const products = [
new Product("Кофе", 600, 10),
new Product("Чай", 450, 5),
new Product("Печенье", 150, 15),
];
products.forEach(product => product.logInfo());

Как это работает? Мы создаем шаблон (конструктор), который определяет свойства и методы будущих объектов. При каждом вызове с new создается новый экземпляр с собственной копией методов.
Классы очень похожи в этом на функции-конструкторы. Они решают те же задачи, но делают код структурированнее, понятнее и элегантнее. Давайте перепишем наш пример с использованием класса:
class Product {
constructor(name, price, discountPercent) {
this.name = name;
this.price = price;
this.discountPercent = discountPercent;
}
getFinalPrice() {
const discountAmount = this.price * (this.discountPercent / 100);
return this.price - discountAmount;
}
logInfo() {
const finalPrice = this.getFinalPrice();
console.log(${this.name}\n Цена: ${this.price} руб.\n Скидка: ${this.discountPercent}%\n Итог: ${finalPrice.toFixed(2)} руб.\n---);
}
}
// Использование остается точно таким же!
const products = [
new Product("Кофе", 600, 10),
new Product("Чай", 450, 5),
new Product("Печенье", 150, 15),
];
products.forEach(product => product.logInfo());

Что изменилось? Код стал чище и логичнее. Все части класса собраны в одном блоке {}. Методы теперь записываются в более привычном виде, без ключевого слова function. А главное, это удобно!
Класс — это современный «синтаксический сахар» в JavaScript, который предоставляет более понятный и структурированный способ создавать шаблоны для объектов. По сути, это красивая обертка над теми же функциями-конструкторами и прототипами, но сгруппированная в единую логическую единицу.
Кстати, синтаксический сахар – это термин, обозначающий специальные элементы синтаксиса языка, которые делают код более удобным для чтения и написания человеком, но не добавляют языку новых функциональных возможностей.
Рассмотрим подробнее синтаксис класса на нашем примере:
class Product— объявление класса с именем (по соглашению с заглавной буквы);
constructor(...)— специальный метод, который вызывается при создании нового экземпляра автоматически черезnew. С помощью него инициализируем свойства;
getFinalPrice() {...} и logInfo() {...}— методы класса. Они автоматически попадают в прототип.
Мы написали свой первый класс. Теперь давайте разберемся с дополнительными возможностями, которые делают классы еще удобнее и превращают обычные свойства в защищенные интерфейсы.

Бесплатный базовый курс по JS
Рассказываем, как работать с переменными, типами данных, функциями и о многом другом!
Геттеры и сеттеры (get/set)
Эти инструменты — приятный бонус, который мы получаем при создании объектов с помощью классов. Они позволяют нам управлять доступом к свойствам объекта более гибко. Прежде чем перейти к коду, давайте разберемся с определениями.
Геттер (get) — это специальный метод класса, который позволяет получать значение свойства, но выглядит и используется как обычное свойство. При его вызове можно выполнить дополнительные вычисления или проверки перед возвратом значения.
Сеттер (set) — это тоже специальный метод класса, но он уже позволяет устанавливать значение свойства. При присваивании значения можно добавить проверки, преобразования или дополнительную логику.
Если сказать коротко, это способ создания «умных» свойств, значения которых можно обработать или проверить перед тем, как их прочитать или изменить.
Давайте добавим в наш класс Product сеттер для свойства price, который будет проверять, что устанавливаемая цена больше нуля. И также добавим геттер, который будет возвращать цену с двумя знаками после запятой (форматированную для вывода).
class Product {
constructor(name, price, discountPercent) {
this.name = name;
this.price = price;
this.discountPercent = discountPercent;
}
getFinalPrice() {
const discountAmount = this.price * (this.discountPercent / 100);
return this.price - discountAmount;
}
logInfo() {
const finalPrice = this.getFinalPrice();
console.log(
`${this.name}\n Цена: ${this.formattedPrice}\n Скидка: ${
this.discountPercent
}%\n Итог: ${finalPrice.toFixed(2)} руб.\n---`
);
}
// Геттер для форматированной цены
get formattedPrice() {
return `${this._price.toFixed(2)} руб.`; // Используем внутреннее свойство
}
// Сеттер для цены с проверкой
set price(value) {
if (value <= 0) {
console.error("Ошибка: цена должна быть больше нуля!");
this._price = 1; // Установим минимальную цену
return;
}
this._price = value;
}
// Геттер для чтения цены
get price() {
return this._price;
}
}
// Давайте проверим, как это работает
const products = [
new Product("Кофе", 600, 10),
new Product("Чай", 450, 5),
new Product("Печенье", 150, 15),
];
// Все работает как обычно
products.forEach((product) => product.logInfo());
// Тестируем сеттер
console.log("\n=== Тестируем защиту от некорректных цен ===");
const testProduct = new Product("Тестовый товар", -100, 10); // Отрицательная цена!
console.log(testProduct.formattedPrice); // 1.00 руб. (была исправлена)
// Меняем цену через сеттер
testProduct.price = 200;
console.log(`Новая цена: ${testProduct.formattedPrice}`); // 200.00 руб.
// Пытаемся установить недопустимое значение
testProduct.price = 0; // Ошибка в консоли
console.log(`Цена после некорректной установки: ${testProduct.formattedPrice}`); // Осталась 200.00 руб.

Да, с первого взгляда код может показаться сложным для понимания. Но если экспериментировать и разобраться глубже, то все проще, чем может казаться.
Как работают сеттеры и геттеры в этом примере
Когда мы создаем новый товар new Product("Кофе", 600, 10), в конструкторе происходит вызов this.price = 600. Это не прямое присваивание, а вызов сеттера set price(value).
Сеттер проверяет, что 600 > 0, и сохраняет значение во внутреннее свойство this._price. Когда нам нужно прочитать цену, например, в методе getFinalPrice(), происходит вызов геттера get price(), который просто возвращает this._price. А геттер formattedPrice преобразует число в красивую строку с надписью «руб.» и двумя знаками после запятой.
Таким образом, мы добавили защиту от некорректных данных и удобное форматирование, не меняя основной логики работы с объектом.
Итог
Классы — это обертка, которая делает наш код чище, понятнее и структурированнее. Мы создали классы с помощью class, инициализировали свойства в конструкторе и объявили методы в едином блоке, что сильно упростило чтение и поддержку кода по сравнению с разрозненными конструкторами. А это уже важный шаг от функций-конструкторов к современному синтаксису классов в JS.
Важно отметить, что мы рассмотрели классы именно как практический инструмент, без углубления в сложные принципы ООП или паттерны проектирования. Мы коснулись полезных возможностей вроде геттеров и сеттеров, которые позволяют контролировать доступ к данным и добавлять логику при чтении или записи свойств. Эти инструменты помогают писать более надежный и выразительный код, не перегружая его излишней сложностью.
Теперь у вас есть прочная основа для дальнейшего изучения. Классы в JavaScript поддерживают и более продвинутые концепции, такие как наследование (с помощью extends), которое позволяет создавать иерархии объектов и писать легко расширяемый код.
А освоив базовый синтаксис, вы сможете постепенно углубляться в возможности объектно-ориентированного подхода, чувствуя себя уверенно благодаря пониманию того, что под капотом все равно работают знакомые вам механизмы.
Сталкивались ли вы уже с наследованием в реальных задачах или пока обходитесь базовой структурой? Пишите о своем опыте и с какими вызовами вы сталкиваетесь в комментариях.
Комментарии (23)

TAZAQ
20.01.2026 09:07А как с точки зрения "сахарности" выглядят приватные поля?

nihil-pro
20.01.2026 09:07Ну что вы неудобные вопросы то задаете? Автор пост написал что бы за час получить плюсики от коллег и забыть, а что там с содержанием не так уж и важно))

js2me
20.01.2026 09:07Не знал что приватные поля уже давно имеют хорошую поддержку во всех браузерах!
Большинство в действительности пользуются TypeScript, а там есть красивое ключевое слово private

TAZAQ
20.01.2026 09:07Жаль, что TS нет в рантайме, private прекрасно отстреливается при компиляции и получается обычный публичный атрибут

difhel
20.01.2026 09:07Как обсуждали в предыдущей статье автора, приватные поля можно было получить и раньше через замыкания (хотя и не стоит использовать такой подход, потому что это ломает оптимизации движков).
Вообще глубоко убежден, что private поля были ошибкой. На практике нередко случается, например, что вам нужно какое-то поле в классе из библиотеки, а разработчик библиотеки
подумал, что он лучше всех знает ООПне подумал о такой возможности и сделал его приватным. Это сильно ухудшает гибкость для сторонних разработчиков. В большинстве языков это нерешаемая проблема, приходится делать форк и поддерживать его в актуальном состоянии. В TS например действительно кейворд private ни на что не влияет в рантайме, что позволяет обойти его через приведение типов, если очень-очень нужно. Чего не скажешь про "#name" поля в JS.
freepad
20.01.2026 09:07Вообще глубоко убеждён, что отказ от глобальных переменных был ошибкой. На практике нередко случается, что вам нужен доступ к какому-то состоянию или флагу в коде библиотеки или фреймворка, а автор решил, что глобальное состояние — это «антипаттерн», и упрятал всё за приватные скоупы, DI или замыкания, не предусмотрев расширяемости. В итоге гибкость для сторонних разработчиков сильно страдает.
/s

mvv-rus
20.01.2026 09:07Это сильно ухудшает гибкость для сторонних разработчиков.
Гибкость не бесплатна: "Червяк может изгибаться как угодно, но в отличие от человека не может стоять." (Конрад Лоренц). В контексте прорграммирования гибкость мешает понижать сложность программ, а именно сложность является главной проблемой разработки сколь-нибудь больших программ (см. например "Мифический человекомесяц"). Идея, лежащая в основе ООП - снизить сложность программы путем разбиения состояния программы на слабо связанные части - объекты. Согласно принципу инкапсуляции из ООП, объекты обладают своим состоянием, являющимся частью общего состояния программы, и взаимодействовать с этим состоянием может не любой код в любом месте программы, а только код методов объекта. Вот за этим и нужны частные (private) поля в объектах.
А в целом гибкость и сложность находятся в противоречии: чем выше гибкость, тем больше вариантов написать программу, то есть - тем больше ее сложность (потенциальная), которую приходится иметь в виду и при чтении кода, и при его написании.И да, для маленьких программ это не важно - сложность их ограничена объемом.

TimurZhoraev
20.01.2026 09:07ООП нужен лишь человеку, чтобы иерархия или глубина вызовов функций влезали в его контекстное окно из 12-15 одновременно располагаемых в голове сущностей. Сложность самой программы от этого не меняется. Любую можно свести на ассемблер. Самое важное что не ввели ни в одном таком языке - это нативная поддержка статического управления, позволяя кодогенераторам или скан-ботам выявлять структуры и управлять ими на этапе разработки, то есть не вручную править классы а соответствующей IDE/редактором по спецификации объектов. По сути все изменения именно в этом механизме. На Питоне по крайней мере что-то сдвинулось с этой точки PEP 729 например. На С++/Java это замороженный 2004 год, когда упёрлись тактовой частотой в потолок и больше одиночным вычислителем уже не подчеркнуть "быстрее, выше, сильнее". Оттого и появилась "компилируемая безопасность" в попытке реализовать эти 5%. В итоге имеем интерпретируемые JSON/XML на все случаи жизни, для виду приправленные ООП, который в контексте пакетной обработки и приведения типов выворачивается наизнанку.

mvv-rus
20.01.2026 09:07ООП нужен лишь человеку, чтобы иерархия или глубина вызовов функций влезали в его контекстное окно из 12-15 одновременно располагаемых в голове сущностей. Сложность самой программы от этого не меняется. Любую можно свести на ассемблер.
Дык, под сложностью программы я как раз и имел в виду сложность ее восприятия/написания человеком. И эта сложность - она, таки да, субъективная. Тренированный человек может держать в голове более сложные программы - например, выявляя их структуру.
PS Остальные ваши мысли - они, может, и интересные, но я не вижу их связи ни со статьей, ни с моим комментарием, а потому оставляю без обсуждения.

cmyser
20.01.2026 09:07Классы топ, особенно для бизнес логики
Так как сохраняют все инварианты внутри себя ( правила бизнеса )
https://youtu.be/ByBzzsnBnAY?si=H41rfKUfV8e9jwd9 мне вот тут нравится как разобрано
А ещё прикольно что в $mol этим классам ещё и накручено кучу всего крутого, по типу локализации, офлайн режима и кучи батареек ещё

TimurZhoraev
20.01.2026 09:07Класс внутри себя ничего не хранит - это абстракция. Хранит физическая ячейка памяти располагаемая в условном массиве, который где-то на листе программиста размечен как класс. Класс может знать о том что он вообще существует, если его ID есть ячейка памяти. Класс может знать что у него есть методы, если есть таблица методов, располагаемых статически или как указатели на функции. Класс знает что он-часть иерархии, если есть таблица виртуальных функций и опять-таки его ID в ней как в линейном массиве или часть switch-case в зависимости от типа объекта при динамическом поведении.
В этом случае действительно разнести класс как спецификацию данных и бизнес-логику (оперирует состоянием автоматови
), которая вообще может не знать какие объекты она использует а работает с их хешами, идентификаторами или правилами обработки типов данных. В этом случае под катом может быть любой язык программирования, так как это всё генерируется автоматически, тем же ИИ-агентом.
То что класс может быть в топе - то есть на вершине стека, можно над этим подумать, действительно, иерархия может быть обработана как стек при размещении туда наследуемых объектов.
mvv-rus
20.01.2026 09:07Класс внутри себя ничего не хранит - это абстракция.
Правильно. А программист - это человек, который приучен работать с абстракциями, причем - на разных их уровнях (если чо, эту мысль я не сам придумал, а подсмотрел у некого Рубена Герра, главного редактора PC Magazine: Russian Edition, году примерно в 1991). На разных уровнях - потому что с абстракциями высокого уровня работать проще, но их область применения ограничена: про абстракции вне области их применимости говорят, что они имеют дыры или протекают (последнее выражение - калька с английского, но используется тоже часто).
Так вот, класс (точнее, в данном случае - его экземпляр) - это абстракция. Переменная (в классе она называется поле) тоже абстракция, но уровнем ниже. Ячейка памяти - тоже абстракция, но на ещё более низком уровне. Абстракция - потому что ни в какой конкретной электронной схеме ячейка памяти не локализована: она может физически в конкретный момент быть как в кэше , так и в оперативной памяти, а то и вообще в странице виртуальной памяти, выгруженной на диск/во флэш, но работает с ней прикладной программист одинаковым образом, пренебрегая этими деталями, пока эти детали не начинают мешать - например, приводить к недопустимо низкой производительности. С классами дело обстоит примерно так же: этой абстракцией можно пользоваться, но следует помнить, что она применима ограниченно.
PS Если вам показалось, что здесь имеет место быть диалектика, то вам не показалось. Но это не важно.

voraa
20.01.2026 09:07В js недоклассы. Нет защищенных полей. Без них бывает туго, если приватное пле в базовом классе надо использовать в наследнике.
Ну и замечание автору. Если учить, то хорошему. Все поля класса должны быть перечислены сразу, а не вводиться по одному там и сям. Что бы при взгляде на класс, сразу было понятно, какие в нем поля.

nihil-pro
20.01.2026 09:07Нет защищенных полей. Без них бывает туго, если приватное пле в базовом классе надо использовать в наследнике
Не согласен на счет «недоклассов».
В js, действительно, нет
protected, но все же такое поведение можно получить, например так:class A { #private = 1; get protected() { return this.#private; } } class B extends A { } console.log(new B().protected) // 1;или еще такой вариант:
class A { #private = 1; static getProtectedValue(instance: A) { return instance.#private; } static setProtectedValue(instance: A, newValue: any) { instance.#private = newValue; } } class B extends A { }; const instance = new B(); console.log(A.getProtectedValue(instance)); // 1; A.setProtectedValue(instance, 2); console.log(A.getProtectedValue(instance)); // 2; // Или так class C extends A { doWork() { return A.getProtectedValue(this); } } console.log(new C().doWork()); // 1;Более того, так как конструкторы также наследуют свойства, то можно и так:
// ... объявление класса А как и выше class B extends A { }; const instance = new B(); // тут уже обращение через B console.log(B.getProtectedValue(instance)); // 1; B.setProtectedValue(instance, 2); console.log(B.getProtectedValue(instance)); // 2; class C extends A { doWork() { // а тут через C return C.getProtectedValue(this); } } console.log(new C().doWork()); // 1;
mrychagov
20.01.2026 09:07Увы, это все еще обычные public свойства, которые можно вызывать у экземпляра класса

markelov69
20.01.2026 09:07Увы, это все еще обычные public свойства, которые можно вызывать у экземпляра класса
Во первых, используйте Typescript и не сможете. Во вторых, и что? Вы же сами пишете код и знаете зачем вам вызов того или иного метода у класса. Раньше было просто условное соглашение __methodName считается приватным и его лучше не вызывать из вне, а methodName публичный

alliumnsk
20.01.2026 09:07Все-таки некоторые вещи, которые можно сделать с помощью class, нельзя сделать с помощью прототипов, так что это не совсем одно и то же.

megahertz
20.01.2026 09:07С классами в JavaScript вышло неловко. В нулевых был большой спрос на ООП в JS. В каждой второй библиотеке был велосипед для наследования. Когда классы добавили, мейнстрим свернул к ФП и классы стали маргинальными.

TimurZhoraev
20.01.2026 09:07Там проблема была искусственная - это попытка прикрутить ООП к пакетной обработке данных, летящих к интерпретатору. Иерархия и классы - железобетонно прибиты идентификаторами к архитектуре. Веб-поведение динамично, объекты могут мутировать до состояния байта. Тем более это для поведения форм - полуфабрикат, который связывается дополнительно с разметкой документа (декларативное представление), которая как-то должна уживаться со спецификацией. Поэтому он ушёл в чисто поведенческую нишу ФП для разметки, фактически став знаком "=" в эксцеле.
Free_ze
Наоборот же: отделена логика инициализации состояния объекта в конструкторе, а описание функций - вынесено отдельно.
Не вижу использования свойства
prototypeв примере с функцией-конструктором, это ведь не эквивалентный код.Бонус - это синтаксис, а сами аксессоры можно добавлять через
Object.defineProperty.