Классы в JavaScript: сахар над прототипами

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Классы появились в ECMAScript 6 и сделали ООП в JavaScript похожим на другие языки. Но важно понимать: это удобная запись поверх прототипов, а не новый механизм.

Базовый класс

class User {
  constructor(name, age) {
    this.name = name;    // this — создаваемый экземпляр
    this.age = age;
  }

  greet() {
    return `Привет, я ${this.name}`;
  }
}

const anna = new User('Аня', 30);
anna.greet();   // "Привет, я Аня"
  • constructor вызывается при new и настраивает новый объект. this внутри него — создаваемый экземпляр (правило new);
  • методы (greet) лежат в прототипе и общие для всех экземпляров — не копируются в каждый объект;
  • вызов через new обязателен: без него this будет не тем.

Это ровно то, что в прототипах писали руками через функцию-конструктор и prototype — класс просто спрятал эти детали.

Наследование: extends и super

class Admin extends User {
  constructor(name, age, level) {
    super(name, age);       // вызвать конструктор родителя — ОБЯЗАТЕЛЬНО
    this.level = level;
  }

  greet() {
    return super.greet() + ` (админ уровня ${this.level})`;   // достроить метод родителя
  }
}

const boss = new Admin('Борис', 40, 3);
boss.greet();   // "Привет, я Борис (админ уровня 3)"

Две роли super:

  • super(...) в конструкторе вызывает конструктор родителя. В наследнике он обязателен до первого обращения к this — иначе ReferenceError. Это не формальность: родитель настраивает базовую часть объекта;
  • super.method() вызывает метод родителя. Это способ достроить поведение, а не заменить: сначала родительский greet, потом своя добавка.

Приватные поля: настоящая приватность

Долгое время приватность в JavaScript была соглашением: подчёркивание в имени (_secret) означало «не трогай», но технически поле оставалось доступным. С приватными полями это изменилось:

class Account {
  #balance = 0;              // приватное поле

  deposit(sum) { this.#balance += sum; }
  getBalance() { return this.#balance; }
}

const acc = new Account();
acc.deposit(100);
acc.getBalance();   // 100
acc.#balance;       // SyntaxError — снаружи не достать!

Поле с префиксом # доступно только внутри класса. Обращение к #balance снаружи — не undefined, а синтаксическая ошибка. Это настоящая инкапсуляция, встроенная в язык, — та же цель, что раньше достигали замыканиями, но привычнее для ООП.

Класс: что публично, что приватно

Публичное

Доступно снаружи

  • Обычные поля: this.name
  • Методы: greet()
  • Геттеры/сеттеры: get, set
  • Часть контракта класса

Приватное (#)

Только внутри класса

  • Поля: #balance
  • Методы: #validate()
  • Снаружи — SyntaxError
  • Внутренняя реализация, можно менять свободно

Статические методы и поля

class User {
  static count = 0;

  constructor(name) {
    this.name = name;
    User.count++;               // считаем всех пользователей
  }

  static fromJSON(json) {       // фабрика
    return new User(JSON.parse(json).name);
  }
}

User.count;                     // сколько создано
const u = User.fromJSON('{"name":"Вера"}');

static принадлежит классу, а не экземпляру. Вызывается как User.fromJSON(), а не user.fromJSON(). Применяют для фабрик (альтернативные способы создать экземпляр), счётчиков и утилит, связанных с классом, но не с конкретным объектом.

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

class Circle {
  constructor(radius) { this.radius = radius; }

  get area() {                        // читается как свойство, а не метод
    return Math.PI * this.radius ** 2;
  }

  set diameter(d) {
    this.radius = d / 2;
  }
}

const c = new Circle(5);
c.area;          // вычисляется при обращении — без ()
c.diameter = 20; // запускает сеттер, radius станет 10

get и set дают вычисляемые свойства: снаружи c.area выглядит как обычное поле, но за ним вычисление. Удобно для производных значений и валидации при записи.

Нужны ли классы вообще

Честный вопрос, потому что JavaScript прекрасно живёт и без них. Правило простое:

  • простые данные — обычный объект или массив, класс избыточен;
  • поведение без состоянияфункции и замыкания;
  • классы оправданы, когда есть несколько экземпляров с общим поведением и своим состоянием, наследование или когда команда работает в ООП-стиле.

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


Смежные темы: прототипы — что под капотом класса; объекты — когда класс не нужен; замыкания — старый способ приватности. Полный список — в уроках JavaScript.

Частые вопросы

Класс в JavaScript — это как в Java?
Внешне похоже, но под капотом это прототипное наследование. class создаёт функцию-конструктор и кладёт методы в prototype. Синтаксис заимствован из классовых языков, механизм остался прототипным.
Как сделать приватное поле?
Через префикс #: class User { #secret = 1 }. Такое поле доступно только внутри класса, снаружи обращение к #secret — синтаксическая ошибка. До этого приватность имитировали замыканиями и соглашением про _ в имени.
Что делает super?
super() в конструкторе вызывает конструктор родителя — обязателен в наследнике до обращения к this. super.method() вызывает метод родителя. Это способ достроить, а не заменить поведение базового класса.
Чем static метод отличается от обычного?
static метод принадлежит самому классу, а не экземпляру: вызывается как Class.method(), а не instance.method(). Удобен для фабрик и утилит, связанных с классом, но не работающих с конкретным объектом.
Нужны ли классы, если есть объекты и функции?
Не всегда. Для простых данных хватает объектов, для поведения — функций и замыканий. Классы оправданы, когда есть несколько экземпляров с общим поведением и состоянием, наследование или когда команда привыкла к ООП.