Классы
Класс в JavaScript — синтаксический сахар над прототипным наследованием. Предоставляет удобный синтаксис для создания объектов с общими методами и поведением.
Зачем нужно
Классы дают знакомый и понятный синтаксис для ООП, обеспечивают strict mode по умолчанию, поддерживают наследование, приватные поля, статические методы. Это стандарт для современных фреймворков и библиотек.
Где используется
React class components, Node.js сервисы, модели данных, игровые объекты, ORM, UI-компоненты, библиотеки.
Предпосылки
Базовый синтаксис
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
Практика
- Создай класс
Rectangleс полями width, height и методами area, perimeter - Добавь статический метод
Rectangle.square(size)— создаёт квадрат - Реализуй приватное поле
#historyдля хранения изменений - Создай getter/setter для валидации размеров
- Напиши класс
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— класс помечен внутренне как класс- Только через
new—Pointбезnew→ TypeError (в отличие от function) - Статика
not enumerable—for (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-конструкторы имеют только первую (см. Пять способов наследования).
Связанные темы
- Прототипы
- Наследование
- Инкапсуляция
- Конструкторы
- Полиморфизм
- Пять способов наследования
- Поведение vs данные в JS
Источники
- Timur · Прототипное программирование и прототипное наследование (2019-11-19) — class vs function
- Timur · Массивы, объекты, классы, прототипы в JavaScript (2018-10-04)
- Timur · Object-oriented programming (2020-02-26)
- MDN — Classes
- JavaScript.info — Классы