Прототипное наследование работает, но код громоздкий: Constructor.prototype.method = function() {...}, Object.create(...), восстановление constructor. Классы решают это: тот же механизм прототипов, но с чистым, привычным синтаксисом. В React, Angular, NestJS классы используются повсеместно.
Класс — это синтаксический сахар над конструктором и прототипами. typeof User === 'function' — класс компилируется в функцию. Под капотом методы класса живут в User.prototype.
new ClassName() работает так же как new Function()class User {
// constructor — специальный метод, вызывается при new User()
constructor(name, email) {
this.name = name
this.email = email
}
// Метод — автоматически попадает в User.prototype
greet() {
return `Привет, я ${this.name}`
}
// toString — переопределение стандартного метода
toString() {
return `[User: ${this.email}]`
}
}
const user = new User('Алексей', 'alex@mail.ru')
console.log(user.greet()) // 'Привет, я Алексей'
console.log(String(user)) // '[User: alex@mail.ru]'
console.log(typeof User) // 'function' — класс это функция!С ES2022 — действительно приватные, недоступные снаружи класса:
class BankAccount {
#balance = 0 // приватное поле
#owner // объявление без значения
constructor(owner, initialBalance = 0) {
this.#owner = owner
this.#balance = initialBalance
}
deposit(amount) {
if (amount <= 0) throw new RangeError('Сумма должна быть > 0')
this.#balance += amount
return this
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Недостаточно средств')
this.#balance -= amount
return this
}
get balance() { return this.#balance }
get owner() { return this.#owner }
}
const acc = new BankAccount('Алексей', 5000)
acc.deposit(3000).withdraw(1000)
console.log(acc.balance) // 7000
// acc.#balance // SyntaxError — приватное поле!Позволяют выглядеть как свойство, но работать как метод:
class Temperature {
#celsius
constructor(celsius) { this.#celsius = celsius }
get fahrenheit() { return this.#celsius * 9/5 + 32 }
set fahrenheit(value) { this.#celsius = (value - 32) * 5/9 }
get celsius() { return this.#celsius }
set celsius(value) {
if (value < -273.15) throw new RangeError('Ниже абсолютного нуля')
this.#celsius = value
}
}
const t = new Temperature(100)
console.log(t.fahrenheit) // 212
t.fahrenheit = 32
console.log(t.celsius) // 0Принадлежат классу, а не экземплярам. Используются как фабрики и утилиты:
class Product {
static #count = 0 // сколько товаров создано
constructor(name, price) {
this.id = ++Product.#count
this.name = name
this.price = price
}
static getCount() {
return Product.#count
}
// Фабричный метод — альтернативный конструктор
static fromJSON(json) {
const { name, price } = typeof json === 'string' ? JSON.parse(json) : json
return new Product(name, price)
}
static compare(a, b) {
return a.price - b.price // для сортировки
}
}
const p1 = new Product('Ноутбук', 75000)
const p2 = Product.fromJSON('{"name":"Мышь","price":1500}')
console.log(Product.getCount()) // 2
console.log([p1, p2].sort(Product.compare).map(p => p.name))
// ['Мышь', 'Ноутбук']Ошибка 1: вызов класса без new
// Сломано:
const u = User('Алексей', 'alex@mail.ru') // TypeError: Class must be called with new
// Исправлено:
const u = new User('Алексей', 'alex@mail.ru')Ошибка 2: поднятие (hoisting) — класс нельзя использовать до объявления
// Сломано:
const u = new User() // ReferenceError — класс ещё не объявлен
class User { }
// Функции можно (function declaration hoisting):
const u = new User() // OK для function User() {}
function User() { }Ошибка 3: потеря this при деструктуризации метода
// Сломано:
const user = new User('Алексей', 'alex@mail.ru')
const { greet } = user
greet() // TypeError — this = undefined в strict mode
// Исправлено — используй стрелочный метод или bind:
class User {
greet = () => `Привет, я ${this.name}` // стрелочное поле — this привязан
}Component, PureComponent@Controller, @Service — декораторы на классахclass User extends Model { } — Sequelize, TypeORMclass AppError extends Error { } — кастомные ошибкиКласс ShoppingCart с приватными полями, геттерами и статическими методами
class ShoppingCart {
#items = []
#discountPercent = 0
static #TAX_RATE = 0.2 // НДС 20%
addItem(product, qty = 1) {
const existing = this.#items.find(i => i.id === product.id)
if (existing) {
existing.qty += qty
} else {
this.#items.push({ ...product, qty })
}
return this // цепочка
}
removeItem(id) {
this.#items = this.#items.filter(i => i.id !== id)
return this
}
applyDiscount(percent) {
if (percent < 0 || percent > 100) throw new RangeError('Скидка от 0 до 100')
this.#discountPercent = percent
return this
}
get subtotal() {
return this.#items.reduce((sum, i) => sum + i.price * i.qty, 0)
}
get discount() {
return Math.round(this.subtotal * this.#discountPercent / 100)
}
get tax() {
return Math.round((this.subtotal - this.discount) * ShoppingCart.#TAX_RATE)
}
get total() {
return this.subtotal - this.discount + this.tax
}
get itemCount() {
return this.#items.reduce((sum, i) => sum + i.qty, 0)
}
getReceipt() {
const lines = this.#items.map(i =>
` ${i.name} x${i.qty} @ ${i.price} ₽ = ${i.price * i.qty} ₽`
)
return [
'--- Чек ---',
...lines,
`Подытог: ${this.subtotal} ₽`,
this.#discountPercent ? `Скидка ${this.#discountPercent}%: -${this.discount} ₽` : null,
`НДС 20%: +${this.tax} ₽`,
`Итого: ${this.total} ₽`,
].filter(Boolean).join('\n')
}
// Статический фабричный метод
static fromItems(items) {
const cart = new ShoppingCart()
items.forEach(({ product, qty }) => cart.addItem(product, qty))
return cart
}
}
const cart = new ShoppingCart()
cart
.addItem({ id: 1, name: 'Ноутбук', price: 75000 }, 1)
.addItem({ id: 2, name: 'Мышь', price: 1500 }, 2)
.addItem({ id: 3, name: 'Коврик', price: 500 }, 1)
.applyDiscount(10)
console.log(`Товаров: ${cart.itemCount}`) // 'Товаров: 4'
console.log(`Подытог: ${cart.subtotal} ₽`) // 'Подытог: 78500 ₽'
console.log(cart.getReceipt())
// --- Чек ---
// Ноутбук x1 @ 75000 ₽ = 75000 ₽
// Мышь x2 @ 1500 ₽ = 3000 ₽
// Коврик x1 @ 500 ₽ = 500 ₽
// Подытог: 78500 ₽
// Скидка 10%: -7850 ₽
// НДС 20%: +14130 ₽
// Итого: 84780 ₽Прототипное наследование работает, но код громоздкий: Constructor.prototype.method = function() {...}, Object.create(...), восстановление constructor. Классы решают это: тот же механизм прототипов, но с чистым, привычным синтаксисом. В React, Angular, NestJS классы используются повсеместно.
Класс — это синтаксический сахар над конструктором и прототипами. typeof User === 'function' — класс компилируется в функцию. Под капотом методы класса живут в User.prototype.
new ClassName() работает так же как new Function()class User {
// constructor — специальный метод, вызывается при new User()
constructor(name, email) {
this.name = name
this.email = email
}
// Метод — автоматически попадает в User.prototype
greet() {
return `Привет, я ${this.name}`
}
// toString — переопределение стандартного метода
toString() {
return `[User: ${this.email}]`
}
}
const user = new User('Алексей', 'alex@mail.ru')
console.log(user.greet()) // 'Привет, я Алексей'
console.log(String(user)) // '[User: alex@mail.ru]'
console.log(typeof User) // 'function' — класс это функция!С ES2022 — действительно приватные, недоступные снаружи класса:
class BankAccount {
#balance = 0 // приватное поле
#owner // объявление без значения
constructor(owner, initialBalance = 0) {
this.#owner = owner
this.#balance = initialBalance
}
deposit(amount) {
if (amount <= 0) throw new RangeError('Сумма должна быть > 0')
this.#balance += amount
return this
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Недостаточно средств')
this.#balance -= amount
return this
}
get balance() { return this.#balance }
get owner() { return this.#owner }
}
const acc = new BankAccount('Алексей', 5000)
acc.deposit(3000).withdraw(1000)
console.log(acc.balance) // 7000
// acc.#balance // SyntaxError — приватное поле!Позволяют выглядеть как свойство, но работать как метод:
class Temperature {
#celsius
constructor(celsius) { this.#celsius = celsius }
get fahrenheit() { return this.#celsius * 9/5 + 32 }
set fahrenheit(value) { this.#celsius = (value - 32) * 5/9 }
get celsius() { return this.#celsius }
set celsius(value) {
if (value < -273.15) throw new RangeError('Ниже абсолютного нуля')
this.#celsius = value
}
}
const t = new Temperature(100)
console.log(t.fahrenheit) // 212
t.fahrenheit = 32
console.log(t.celsius) // 0Принадлежат классу, а не экземплярам. Используются как фабрики и утилиты:
class Product {
static #count = 0 // сколько товаров создано
constructor(name, price) {
this.id = ++Product.#count
this.name = name
this.price = price
}
static getCount() {
return Product.#count
}
// Фабричный метод — альтернативный конструктор
static fromJSON(json) {
const { name, price } = typeof json === 'string' ? JSON.parse(json) : json
return new Product(name, price)
}
static compare(a, b) {
return a.price - b.price // для сортировки
}
}
const p1 = new Product('Ноутбук', 75000)
const p2 = Product.fromJSON('{"name":"Мышь","price":1500}')
console.log(Product.getCount()) // 2
console.log([p1, p2].sort(Product.compare).map(p => p.name))
// ['Мышь', 'Ноутбук']Ошибка 1: вызов класса без new
// Сломано:
const u = User('Алексей', 'alex@mail.ru') // TypeError: Class must be called with new
// Исправлено:
const u = new User('Алексей', 'alex@mail.ru')Ошибка 2: поднятие (hoisting) — класс нельзя использовать до объявления
// Сломано:
const u = new User() // ReferenceError — класс ещё не объявлен
class User { }
// Функции можно (function declaration hoisting):
const u = new User() // OK для function User() {}
function User() { }Ошибка 3: потеря this при деструктуризации метода
// Сломано:
const user = new User('Алексей', 'alex@mail.ru')
const { greet } = user
greet() // TypeError — this = undefined в strict mode
// Исправлено — используй стрелочный метод или bind:
class User {
greet = () => `Привет, я ${this.name}` // стрелочное поле — this привязан
}Component, PureComponent@Controller, @Service — декораторы на классахclass User extends Model { } — Sequelize, TypeORMclass AppError extends Error { } — кастомные ошибкиКласс ShoppingCart с приватными полями, геттерами и статическими методами
class ShoppingCart {
#items = []
#discountPercent = 0
static #TAX_RATE = 0.2 // НДС 20%
addItem(product, qty = 1) {
const existing = this.#items.find(i => i.id === product.id)
if (existing) {
existing.qty += qty
} else {
this.#items.push({ ...product, qty })
}
return this // цепочка
}
removeItem(id) {
this.#items = this.#items.filter(i => i.id !== id)
return this
}
applyDiscount(percent) {
if (percent < 0 || percent > 100) throw new RangeError('Скидка от 0 до 100')
this.#discountPercent = percent
return this
}
get subtotal() {
return this.#items.reduce((sum, i) => sum + i.price * i.qty, 0)
}
get discount() {
return Math.round(this.subtotal * this.#discountPercent / 100)
}
get tax() {
return Math.round((this.subtotal - this.discount) * ShoppingCart.#TAX_RATE)
}
get total() {
return this.subtotal - this.discount + this.tax
}
get itemCount() {
return this.#items.reduce((sum, i) => sum + i.qty, 0)
}
getReceipt() {
const lines = this.#items.map(i =>
` ${i.name} x${i.qty} @ ${i.price} ₽ = ${i.price * i.qty} ₽`
)
return [
'--- Чек ---',
...lines,
`Подытог: ${this.subtotal} ₽`,
this.#discountPercent ? `Скидка ${this.#discountPercent}%: -${this.discount} ₽` : null,
`НДС 20%: +${this.tax} ₽`,
`Итого: ${this.total} ₽`,
].filter(Boolean).join('\n')
}
// Статический фабричный метод
static fromItems(items) {
const cart = new ShoppingCart()
items.forEach(({ product, qty }) => cart.addItem(product, qty))
return cart
}
}
const cart = new ShoppingCart()
cart
.addItem({ id: 1, name: 'Ноутбук', price: 75000 }, 1)
.addItem({ id: 2, name: 'Мышь', price: 1500 }, 2)
.addItem({ id: 3, name: 'Коврик', price: 500 }, 1)
.applyDiscount(10)
console.log(`Товаров: ${cart.itemCount}`) // 'Товаров: 4'
console.log(`Подытог: ${cart.subtotal} ₽`) // 'Подытог: 78500 ₽'
console.log(cart.getReceipt())
// --- Чек ---
// Ноутбук x1 @ 75000 ₽ = 75000 ₽
// Мышь x2 @ 1500 ₽ = 3000 ₽
// Коврик x1 @ 500 ₽ = 500 ₽
// Подытог: 78500 ₽
// Скидка 10%: -7850 ₽
// НДС 20%: +14130 ₽
// Итого: 84780 ₽Создай класс EventEmitter — упрощённый аналог Node.js EventEmitter. Приватное поле #handlers хранит Map<eventName, Set<handler>>. Методы: on(event, handler) — подписаться на событие, off(event, handler) — отписаться, emit(event, ...args) — вызвать всех подписчиков события, once(event, handler) — подписаться только на одно срабатывание (после первого вызова автоматически отписаться).
off: this.#handlers.get(event)?.delete(handler). emit: handler(...args) в forEach. once: в wrapper после вызова handler вызови this.off(event, wrapper) — отписываем саму обёртку.