Классы

Класс в JavaScript — синтаксический сахар над прототипным наследованием. Предоставляет удобный синтаксис для создания объектов с общими методами и поведением.

Зачем нужно

Классы дают знакомый и понятный синтаксис для ООП, обеспечивают strict mode по умолчанию, поддерживают наследование, приватные поля, статические методы. Это стандарт для современных фреймворков и библиотек.

Где используется

React class components, Node.js сервисы, модели данных, игровые объекты, ORM, UI-компоненты, библиотеки.

Предпосылки

Прототипы, this, Конструкторы

Базовый синтаксис

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

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

  getInfo {
    return `${this.name} (${this.email})`;
  }
}

const user = new User('Иван', 'ivan@example.com');
console.log(user.greet);   // "Привет, Иван!"
console.log(user.getInfo); // "Иван (ivan@example.com)"

// Под капотом — прототип
console.log(typeof User); // "function"
console.log(user.__proto__ === User.prototype); // true

Constructor

class Product {
  constructor(name, price) {
    // Валидация в конструкторе
    if (!name) throw new Error('Имя обязательно');
    if (price < 0) throw new Error('Цена не может быть отрицательной');

    this.name = name;
    this.price = price;
    this.createdAt = new Date();
  }
}

const item = new Product('Книга', 500);
// new Product('', 100); // Error: Имя обязательно

Методы

class Calculator {
  constructor(initial = 0) {
    this.value = initial;
  }

  // Обычные методы — попадают в prototype
  add(n) {
    this.value += n;
    return this; // для цепочки вызовов
  }

  subtract(n) {
    this.value -= n;
    return this;
  }

  multiply(n) {
    this.value *= n;
    return this;
  }

  getResult {
    return this.value;
  }
}

const result = new Calculator(10)
  .add(5)
  .multiply(2)
  .subtract(3)
  .getResult;
console.log(result); // 27

Статические методы и свойства

class MathUtils {
  static PI = 3.14159265;

  static square(x) {
    return x * x;
  }

  static clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }

  static random(min, max) {
    return Math.floor(Math.random * (max - min + 1)) + min;
  }
}

// Вызов без создания экземпляра
console.log(MathUtils.square(5));      // 25
console.log(MathUtils.clamp(15, 0, 10)); // 10
console.log(MathUtils.PI);             // 3.14159265

// const m = new MathUtils();
// m.square(5); // TypeError — статические методы недоступны экземпляру

Фабричный метод

class User {
  constructor(name, role) {
    this.name = name;
    this.role = role;
  }

  static createAdmin(name) {
    return new User(name, 'admin');
  }

  static createGuest {
    return new User('Гость', 'guest');
  }

  static fromJSON(json) {
    const data = typeof json === 'string' ? JSON.parse(json) : json;
    return new User(data.name, data.role);
  }
}

const admin = User.createAdmin('Иван');
const guest = User.createGuest;
const fromData = User.fromJSON('{"name":"Мария","role":"editor"}');

Getters и Setters

class Temperature {
  #celsius;

  constructor(celsius) {
    this.celsius = celsius; // использует setter
  }

  get celsius {
    return this.#celsius;
  }

  set celsius(value) {
    if (value < -273.15) {
      throw new Error('Температура ниже абсолютного нуля');
    }
    this.#celsius = value;
  }

  get fahrenheit {
    return this.#celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212
temp.fahrenheit = 32;
console.log(temp.celsius);    // 0

Приватные поля и методы (#)

class BankAccount {
  #balance;
  #pin;
  #transactionLog = ;

  constructor(owner, initialBalance, pin) {
    this.owner = owner;
    this.#balance = initialBalance;
    this.#pin = pin;
  }

  #validatePin(pin) {
    if (pin !== this.#pin) {
      throw new Error('Неверный PIN');
    }
  }

  #log(action, amount) {
    this.#transactionLog.push({
      action, amount,
      balance: this.#balance,
      date: new Date
    });
  }

  deposit(amount) {
    this.#balance += amount;
    this.#log('deposit', amount);
    return this.#balance;
  }

  withdraw(amount, pin) {
    this.#validatePin(pin);
    if (amount > this.#balance) throw new Error('Недостаточно средств');
    this.#balance -= amount;
    this.#log('withdraw', amount);
    return this.#balance;
  }

  get balance {
    return this.#balance;
  }
}

const account = new BankAccount('Иван', 1000, '1234');
account.deposit(500);          // 1500
account.withdraw(200, '1234'); // 1300
console.log(account.balance);  // 1300
// account.#balance;           // SyntaxError — приватное поле
// account.#validatePin('1234'); // SyntaxError

Computed Property Names

const METHOD_NAME = 'dynamicMethod';

class Dynamic {
  [METHOD_NAME] {
    return 'Вызван динамический метод';
  }

  ['get' + 'Data'] {
    return { id: 1 };
  }

  [Symbol.toPrimitive](hint) {
    if (hint === 'string') return 'Dynamic object';
    return 42;
  }
}

const d = new Dynamic();
console.log(d.dynamicMethod); // "Вызван динамический метод"
console.log(d.getData);       // { id: 1 }
console.log(`${d}`);            // "Dynamic object"

Class Fields (публичные)

class Counter {
  count = 0;                    // публичное поле
  label = 'Счётчик';

  // Arrow method — автоматический bind this
  increment = () => {
    this.count++;
    console.log(`${this.label}: ${this.count}`);
  };
}

const counter = new Counter();
const { increment } = counter;
increment; // "Счётчик: 1" — this не потерян благодаря arrow

Класс — это выражение

// Class Expression
const MyClass = class {
  constructor(value) {
    this.value = value;
  }
};

// Named Class Expression
const Foo = class Bar {
  constructor {
    console.log(Bar.name); // "Bar" — доступно внутри
  }
};
// console.log(Bar); // ReferenceError — Bar не видно снаружи

// Динамическое создание
function createClass(methods) {
  return class {
    constructor(data) {
      Object.assign(this, data);
    }
    ...methods
  };
}

Отличия класса от функции-конструктора

Аспект class function
Hoisting Нет (TDZ) Да
strict mode Всегда Нужен явно
Вызов без new TypeError Работает (баг-источник)
Перечисляемость методов Нет Да
Синтаксис extends Встроенный Ручной через prototype

Частые ошибки

1. Вызов класса без new

// class User { constructor(name) { this.name = name; } }
// const u = User('Иван'); // TypeError: Class constructor cannot be invoked without 'new'

2. Обращение к классу до объявления

// const u = new User('Иван'); // ReferenceError — нет hoisting
// class User { ... }

3. this в методе-callback

class Logger {
  prefix = '[LOG]';
  log(message) {
    console.log(`${this.prefix} ${message}`);
  }
}
const logger = new Logger();
// setTimeout(logger.log, 100, 'тест'); // "[undefined] тест"

// Решение: arrow field или bind

Практика

  1. Создай класс Rectangle с полями width, height и методами area, perimeter
  2. Добавь статический метод Rectangle.square(size) — создаёт квадрат
  3. Реализуй приватное поле #history для хранения изменений
  4. Создай getter/setter для валидации размеров
  5. Напиши класс TodoList с методами add, remove, toggle, getAll

Классы — сахар над прототипами

«Под капотом класса скрыта другая машинерия — прототипная. Синтаксис позволяет нам быстро и удобно наполнять прототипы методами» (SzaXTW2qcJE).

class Point {
  constructor(x, y) { this.x = x; this.y = y; }
  move(dx, dy) { this.x += dx; this.y += dy; }
  static from(p) { return new Point(p.x, p.y); }
}
// Эквивалентно:
function Point(x, y) { this.x = x; this.y = y; }
Point.prototype.move = function(dx, dy) { this.x += dx; this.y += dy; };
Point.from = function(p) { return new Point(p.x, p.y); };

Отличия class от function-конструктора

  • kind: class — класс помечен внутренне как класс
  • Только через newPoint без new → TypeError (в отличие от function)
  • Статика not enumerablefor (const k in Point) не покажет from
  • Square.__proto__ === Rect при extends Rect — не Function.prototype. Это даёт наследование статических методов
  • Метод-определения автоматически not enumerable в class { foo {} }

Две цепочки при class extends

class Rect {}
class Square extends Rect {}

Square.prototype.__proto__ === Rect.prototype;  // цепочка для методов
Square.__proto__           === Rect;            // цепочка для статики

Function-конструкторы имеют только первую (см. Пять способов наследования).

Связанные темы

Источники