Класс в JavaScript — это шаблон для создания объектов с общими данными и поведением, который объявляется ключевым словом class и используется через new. Под капотом класс остается функцией-конструктором, а методы лежат в прототипе: class (появился в ES2015) — удобный синтаксис над прототипным наследованием, а не отдельная модель, как в Java или C#.
Содержание
Ниже — полный рабочий пример, затем по порядку: конструктор и поля, методы, геттеры и сеттеры, static, приватные члены #, наследование через extends и super, и типичные ошибки. Все примеры проверены в Node.js 24.21 (ветка LTS) 24.09.2026; в современных браузерах синтаксис тот же.
Три слова, которые легко спутать:
- класс — сам шаблон (
User); - экземпляр — конкретный объект, созданный из него (
new User("Анна")); - прототип — объект
User.prototype, где хранятся общие для всех экземпляров методы.
Аналогия: класс — чертеж дома, экземпляр — построенный по нему дом, прототип — общая для всех домов инструкция по эксплуатации.
Минимальный пример класса
class User {
role = "student"; // публичное поле с умолчанием
#password; // приватное поле
constructor(name, password) {
this.name = name;
this.#password = password;
}
greet() {
return `Привет, ${this.name} (${this.role})`;
}
checkPassword(value) {
return this.#password === value;
}
}
const anna = new User("Анна", "s3cret");
console.log(anna.greet());
console.log(anna.checkPassword("123"));
console.log(anna.checkPassword("s3cret"));
console.log(Object.keys(anna));
В консоли появится:
Привет, Анна (student)
false
true
[ 'role', 'name' ]
Что здесь происходит:
new User(...)создает пустой объект, связывает его сUser.prototypeи вызываетconstructor, гдеthis— этот новый объект. Это упрощение для базового класса: у наследника объект создает конструктор родителя в момент вызоваsuper(), поэтому до негоthisнедоступен (см. ошибку 1 ниже).- Поля, объявленные в теле класса (
role,#password), инициализируются до выполнения тела конструктора (у наследника — сразу послеsuper()) — поэтому вObject.keysсначалаrole, потомname. #passwordне попадает вObject.keysи не виден снаружи: прочитать его может только код внутри класса.- Если конструктор не написать, JavaScript создаст пустой конструктор по умолчанию (у наследника — такой, что передает все аргументы в
super).
Пароль здесь хранится открытым текстом только ради простоты примера: в реальном приложении пароли не хранят в объектах и не сравнивают напрямую, этим занимается сервер с хешированием.
Класс — это синтаксис над прототипами
Проверим утверждение из определения кодом:
class User {
constructor(name) { this.name = name; }
greet() { return `Привет, ${this.name}`; }
}
const u = new User("Анна");
console.log(typeof User); // function
console.log(Object.getPrototypeOf(u) === User.prototype); // true
console.log(Object.hasOwn(u, "greet")); // false
console.log(Object.hasOwn(User.prototype, "greet")); // true
// Почти то же самое без class:
function OldUser(name) { this.name = name; }
OldUser.prototype.greet = function () { return `Привет, ${this.name}`; };
console.log(new OldUser("Иван").greet());
Вывод: function, true, false, true, Привет, Иван. Метод greet один на всех — он в прототипе, а не копируется в каждый объект.
«Почти то же самое» — не опечатка. У класса есть отличия от функции-конструктора:
| Свойство | class |
function + prototype |
|---|---|---|
Вызов без new |
TypeError |
вызывается как обычная функция: this — глобальный объект или undefined в строгом режиме |
| Использование до объявления | ReferenceError |
функция поднимается (hoisting) |
| Строгий режим внутри | всегда | только с "use strict" |
Методы в for...in |
не перечисляются | перечисляются, если добавлены присваиванием |
Приватные поля #, static-блоки |
есть | нет |
Методы, геттеры и сеттеры
Метод экземпляра — функция в теле класса: greet() {...}. Внутри него this указывает на объект, у которого метод вызвали (anna.greet() — значит, anna).
Геттер и сеттер выглядят снаружи как обычное свойство, но при чтении и записи выполняют код. Типичная задача — проверить значение перед записью:
class Temperature {
#celsius = 0;
get celsius() { return this.#celsius; }
set celsius(v) {
if (typeof v !== "number" || Number.isNaN(v)) {
throw new TypeError(`Ожидалось число, получено: ${v}`);
}
if (v < -273.15) {
throw new RangeError("Ниже абсолютного нуля");
}
this.#celsius = v;
}
get fahrenheit() { return this.#celsius * 9 / 5 + 32; }
}
const t = new Temperature();
t.celsius = 25;
console.log(t.celsius, t.fahrenheit); // 25 77
try {
t.celsius = -300;
} catch (e) {
console.log(e.name, e.message);
}
Вывод: 25 77 и RangeError Ниже абсолютного нуля. Значение хранится в приватном #celsius, а сеттер — единственный путь его изменить. У fahrenheit только геттер: это вычисляемое свойство «только для чтения».
static, приватные методы и static-блоки
Статические члены принадлежат самому классу, а не экземпляру: счетчики, константы, фабричные и служебные функции. Приватные члены с # доступны только внутри тела класса; это может быть поле, метод, геттер, в том числе статический.
class Counter {
static created = 0; // статическое поле: одно на класс
static #secretPrefix = "id"; // статическое приватное поле
static defaults;
static { // статический блок: выполняется один раз при создании класса
Counter.defaults = { step: 1, max: 3 };
}
#value = 0;
constructor() {
Counter.created += 1;
this.id = `${Counter.#secretPrefix}-${Counter.created}`;
}
#clamp(n) { // приватный метод
return Math.min(n, Counter.defaults.max);
}
inc() {
this.#value = this.#clamp(this.#value + Counter.defaults.step);
return this;
}
get value() { return this.#value; }
static isCounter(obj) { return #value in obj; }
}
const c = new Counter();
c.inc().inc().inc().inc();
console.log(c.id, c.value); // id-1 3
new Counter();
console.log(Counter.created); // 2
console.log(c.created); // undefined
console.log(Counter.isCounter(c), Counter.isCounter({ value: 3 }));
Вывод по строкам: id-1 3, 2, undefined, true false. Разбор:
Counter.createdчитается через имя класса; через экземплярc.created—undefined.- Четыре вызова
inc()упираются вmax: 3благодаря приватному методу#clamp.incвозвращаетthis, поэтому вызовы складываются в цепочку. - Статический блок
static {...}удобен, когда инициализация статических полей требует нескольких шагов (например, заполнить справочник). #value in obj— проверка «прошел ли объект через конструктор этого класса»: у обычного объекта с полемvalueприватного#valueнет. Экземпляр наследника (class Sub extends Counter) эту проверку тоже проходит, так что это не строгое «ровно этот класс», а «этот класс или его потомок».
Приватность # обеспечивает сам язык, а не соглашение вроде подчеркивания _name: снаружи u["#password"] вернет undefined, а JSON.stringify приватные поля не выводит. Граница: это защита от случайного доступа из другого кода, а не средство хранения секретов — все, что попало в браузер, пользователь может увидеть в отладчике.
Наследование: extends и super
extends делает один класс наследником другого: экземпляр получает методы родителя через цепочку прототипов. super(...) вызывает конструктор родителя, super.method() — метод родителя.
class User {
constructor(name) { this.name = name; }
describe() { return `Пользователь ${this.name}`; }
}
class Admin extends User {
constructor(name, level) {
super(name); // сначала конструктор родителя
this.level = level; // только после super() можно трогать this
}
describe() {
return `${super.describe()}, уровень доступа ${this.level}`;
}
}
const a = new Admin("Олег", 2);
console.log(a.describe());
console.log(a instanceof Admin, a instanceof User);
console.log(Object.getPrototypeOf(Admin.prototype) === User.prototype);
Вывод:
Пользователь Олег, уровень доступа 2
true true
true
Admin переопределил describe, но переиспользовал родительскую версию через super.describe(). Приватные поля родителя наследнику напрямую недоступны: #-имя видно только в теле того класса, где объявлено, поэтому наследник работает с ними через публичные методы или геттеры родителя.
Наследование удобно для настоящего отношения «является» (администратор является пользователем). Если связь другая — «содержит», «использует» — обычно проще передать объект в поле, чем строить глубокую иерархию.
Типичные ошибки с классами
1. Обращение к this до super().
class User {
constructor(name) { this.name = name; }
}
class Admin extends User {
constructor(name, level) {
this.level = level;
super(name);
}
}
new Admin("Олег", 2);
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
Исправление: в конструкторе наследника первой строкой, которая касается this, должен идти super(...), как в примере Admin выше.
2. Вызов класса без new.
class User {
constructor(name) { this.name = name; }
}
const u = User("Анна");
TypeError: Class constructor User cannot be invoked without 'new'
Исправление: const u = new User("Анна");. Похожая ошибка — создать экземпляр до строки с class: объявление класса не поднимается, и Node.js выдаст ReferenceError: Cannot access 'User' before initialization.
3. Потеря this в колбэке. Метод, переданный как отдельная функция (в обработчик события, setTimeout, map), теряет связь с объектом:
class Button {
constructor(label) { this.label = label; }
handleClick() { return `Нажата кнопка ${this.label}`; }
}
const b = new Button("OK");
const handler = b.handleClick; // метод оторван от объекта
console.log(handler());
TypeError: Cannot read properties of undefined (reading 'label')
Тело класса всегда в строгом режиме, поэтому this в оторванном методе — undefined, а не глобальный объект. Три способа исправить:
class Button {
constructor(label) { this.label = label; }
handleClick = () => `Нажата кнопка ${this.label}`; // поле-стрелка: this привязан к экземпляру
}
const b = new Button("OK");
const handler = b.handleClick;
console.log(handler());
class Button2 {
constructor(label) { this.label = label; }
handleClick() { return `Нажата кнопка ${this.label}`; }
}
const b2 = new Button2("Отмена");
console.log(b2.handleClick.bind(b2)());
setTimeout(() => console.log(b2.handleClick()), 0);
Вывод: Нажата кнопка OK, затем дважды Нажата кнопка Отмена. Как выбрать: поле-стрелка удобно для обработчиков, но создает отдельную функцию в каждом экземпляре; bind или стрелка-обертка в месте вызова оставляют метод в прототипе.
4. Приватное поле снаружи класса. Код console.log(u.#password) вне тела класса даже не запустится: Node.js останавливается на разборе файла с SyntaxError: Private field '#password' must be declared in an enclosing class. Нужен публичный метод или геттер.
5. Запись в свойство только с геттером. Если у свойства нет сеттера (c.value = 100 в примере Counter), результат зависит от режима файла: в обычном скрипте присваивание молча игнорируется, в ES-модуле (строгий режим) — TypeError: Cannot set property value of #<Counter> which has only a getter.
Выводы
- Класс в JavaScript объявляется через
class, экземпляр создается черезnew; под капотом это функция, а методы хранятся в прототипе. - Поля в теле класса инициализируются до кода конструктора;
#дает приватность на уровне языка,static— члены самого класса,static {}— однократную инициализацию. - Наследник объявляется через
extends, а в его конструктореsuper(...)обязан идти до обращения кthis. - Самые частые ошибки — вызов без
new, обращение кthisдоsuper()и потеряthisпри передаче метода в колбэк.
Где применяется / связь с практикой
Классы встречаются во фронтенде и на Node.js: модели данных, сервисы работы с API, собственные ошибки (class NotFoundError extends Error), веб-компоненты (class MyWidget extends HTMLElement). Хорошее упражнение — переписать объект с функциями в класс с приватным состоянием и геттерами, а потом вынести общее поведение в родителя.
Освойте тему на практике
Если хочется разобрать классы, прототипы и this вместе с остальной базой языка на задачах с проверкой, посмотри курс Otus по JavaScript с нуля. Отдельные темы удобно попробовать на бесплатных открытых уроках Otus.
FAQ
Можно ли объявить класс выражением, без имени?
Да: const User = class { ... };. Такой класс ведет себя так же, его имя берется из переменной (User.name === "User").
Есть ли в JavaScript абстрактные классы и интерфейсы?
Ключевых слов для них в языке нет. Абстрактность обычно имитируют проверкой new.target === Base в конструкторе с выбросом ошибки, а интерфейсы описывают в TypeScript.
Чем приватное поле #name отличается от _name?
_name — только соглашение, свойство доступно всем и видно в Object.keys. #name недоступно вне тела класса на уровне синтаксиса.



