البرمجة كائنية التوجه: الفئات والكائنات في TypeScript — دليل شامل
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — архитектор, который проектирует дома. Ты не можешь каждый раз изобретать колесо: сначала придумывать, где будут стены, потом — как открываются двери, потом — какой формы окна. Вместо этого ты создаёшь чертёж дома. На чертеже указано: у дома будет 4 стены, 2 двери, 5 окон, крыша. Но сам чертёж — это ещё не дом. Это просто описание.
Когда приходит заказчик, ты берёшь этот чертёж и строишь по нему конкретный дом. Потом приходит второй заказчик — ты строишь второй дом по тому же чертежу. Дома могут отличаться цветом стен или материалом крыши, но структура у них одинаковая.
В программировании работает та же логика:
- Класс — это чертёж. Он описывает, какие свойства (цвет, размер, имя) и методы (действия: идти, говорить, считать) будут у объекта.
- Объект — это конкретный дом, построенный по чертежу. У каждого объекта свои значения свойств, но структура — из класса.
💡 Важно: Класс — это тип данных, который ты сам придумываешь. В TypeScript классы помогают организовать код так, чтобы он был похож на реальный мир: у тебя есть «сущности» (пользователи, машины, заказы), и каждая сущность знает, что она умеет делать.
Зачем это нужно? Представь, что ты пишешь игру. У тебя 100 врагов. Без классов тебе пришлось бы 100 раз писать одно и то же: «у этого врага 10 жизней, он умеет атаковать, он умеет получать урон». С классами ты пишешь один раз чертёж «Враг», а потом создаёшь 100 объектов-врагов. Если нужно добавить всем врагам новое умение — ты меняешь только класс, и все 100 врагов получают это умение автоматически.
Теория: как это работает изнутри
Когда ты объявляешь класс, TypeScript (а под капотом — JavaScript) создаёт функцию-конструктор. Эта функция умеет создавать новые объекты и наделять их свойствами и методами.
Внутри класса есть три ключевых элемента:
- Поля (свойства) — переменные, которые принадлежат объекту. Например,
name: stringилиage: number. - Конструктор — специальный метод
constructor(), который вызывается автоматически, когда ты создаёшь новый объект. В нём ты задаёшь начальные значения свойств. - Методы — функции, которые принадлежат объекту. Они могут менять свойства объекта или выполнять какие-то действия.
Когда ты пишешь new ClassName(), происходит следующее:
- Создаётся пустой объект.
- К этому объекту привязываются все методы класса (через прототип).
- Вызывается конструктор, который заполняет свойства объекта.
- Объект возвращается и сохраняется в переменную.
⚠️ Важно: В TypeScript есть модификаторы доступа:public(доступно всем),private(доступно только внутри класса),protected(доступно внутри класса и его наследников). Это защищает данные от случайного изменения.
Синтаксис в TypeScript: все формы и варианты
Давай разберём синтаксис класса от простого к сложному. Вот базовая структура:
// Объявляем класс с именем "Person"
class Person {
// Свойства (поля) с указанием типов
name: string;
age: number;
// Конструктор — вызывается при создании объекта
constructor(name: string, age: number) {
this.name = name; // this — это сам объект
this.age = age;
}
// Метод — функция внутри класса
greet(): void {
console.log(`Привет, я ${this.name}!`);
}
}
// Создаём объект (экземпляр класса)
const person1 = new Person("Анна", 25);
person1.greet(); // Выведет: "Привет, я Анна!"
Варианты синтаксиса:
| Конструкция | Описание | Пример |
|---|---|---|
| Свойства с модификаторами | Указываем доступ: public, private, protected | private age: number |
| Сокращённая запись свойств | Свойства объявляются прямо в конструкторе | constructor(public name: string) |
| Статические свойства/методы | Принадлежат классу, а не объекту | static count: number = 0 |
| Геттеры и сеттеры | Контролируемый доступ к свойствам | get fullName() |
| Абстрактные классы | Нельзя создать объект, только наследовать | abstract class Animal |
Сокращённая запись свойств в конструкторе:
class User {
// Свойства объявлены прямо в параметрах конструктора
constructor(
public name: string,
private age: number,
protected email: string
) {}
public getAge(): number {
return this.age; // Доступно, потому что мы внутри класса
}
}
const user = new User("Иван", 30, "ivan@mail.ru");
console.log(user.name); // ✅ Доступно (public)
// console.log(user.age); // ❌ Ошибка: age — private
// console.log(user.email); // ❌ Ошибка: email — protected
Примеры кода — от простого к сложному
Пример 1: Простейший класс "Книга"
// Класс описывает книгу
class Book {
title: string; // Название книги
author: string; // Автор
pages: number; // Количество страниц
// Конструктор: задаём начальные значения
constructor(title: string, author: string, pages: number) {
this.title = title;
this.author = author;
this.pages = pages;
}
// Метод: выводит информацию о книге
getSummary(): string {
return `"${this.title}" — ${this.author}, ${this.pages} стр.`;
}
}
// Создаём объект книги
const myBook = new Book("Война и мир", "Лев Толстой", 1225);
console.log(myBook.getSummary()); // "Война и мир" — Лев Толстой, 1225 стр.
Пример 2: Класс с приватными полями и геттерами
class BankAccount {
// Приватное поле: доступно только внутри класса
private _balance: number;
constructor(initialBalance: number) {
this._balance = initialBalance;
}
// Геттер — позволяет читать приватное поле
get balance(): number {
return this._balance;
}
// Сеттер — позволяет безопасно менять приватное поле
set balance(amount: number) {
if (amount < 0) {
console.log("Баланс не может быть отрицательным!");
return;
}
this._balance = amount;
}
// Метод для пополнения счёта
deposit(amount: number): void {
if (amount > 0) {
this._balance += amount;
console.log(`Пополнено на ${amount}. Баланс: ${this._balance}`);
}
}
}
const account = new BankAccount(1000);
console.log(account.balance); // 1000 (через геттер)
account.deposit(500); // Пополнено на 500. Баланс: 1500
account.balance = 2000; // Используем сеттер
console.log(account.balance); // 2000
Пример 3: Статические члены класса
class MathHelper {
// Статическое свойство — принадлежит классу, а не объекту
static pi: number = 3.14159;
// Статический метод — вызывается без создания объекта
static circleArea(radius: number): number {
return MathHelper.pi * radius * radius;
}
}
// Вызываем статический метод напрямую через класс
const area = MathHelper.circleArea(5);
console.log(`Площадь круга: ${area}`); // Площадь круга: 78.53975
// Создавать объект не нужно!
// const helper = new MathHelper(); // ❌ Не имеет смысла
Пример 4: Наследование классов
// Базовый класс "Животное"
class Animal {
constructor(public name: string) {}
makeSound(): void {
console.log(`${this.name} издаёт звук`);
}
}
// Дочерний класс "Собака" наследует Animal
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Вызываем конструктор родителя
}
// Переопределяем метод родителя
makeSound(): void {
console.log(`${this.name} лает: Гав-гав!`);
}
// Новый метод, которого нет у Animal
fetch(): void {
console.log(`${this.name} приносит палку`);
}
}
const dog = new Dog("Бобик", "Овчарка");
dog.makeSound(); // Бобик лает: Гав-гав!
dog.fetch(); // Бобик приносит палку
Пример 5: Абстрактный класс
// Абстрактный класс — нельзя создать объект, только наследовать
abstract class Shape {
constructor(public color: string) {}
// Абстрактный метод — должен быть реализован в наследнике
abstract getArea(): number;
// Обычный метод — доступен всем наследникам
getColor(): string {
return this.color;
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
// Обязаны реализовать абстрактный метод
getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
const circle = new Circle("красный", 10);
console.log(circle.getArea()); // 314.159...
// const shape = new Shape("синий"); // ❌ Ошибка: нельзя создать объект абстрактного класса
Пример 6: Реализация интерфейса
// Интерфейс — описывает структуру, но не содержит реализации
interface Flyable {
fly(): void;
land(): void;
}
// Класс реализует интерфейс
class Airplane implements Flyable {
fly(): void {
console.log("Самолёт летит");
}
land(): void {
console.log("Самолёт приземляется");
}
}
class Bird implements Flyable {
fly(): void {
console.log("Птица машет крыльями");
}
land(): void {
console.log("Птица садится на ветку");
}
}
// Полиморфизм: работаем с разными классами через общий интерфейс
function startFlight(flyable: Flyable): void {
flyable.fly();
}
startFlight(new Airplane()); // Самолёт летит
startFlight(new Bird()); // Птица машет крыльями
Пример 7: Композиция классов
class Engine {
start(): void {
console.log("Двигатель запущен");
}
}
class Wheels {
rotate(): void {
console.log("Колёса крутятся");
}
}
// Класс Car использует другие классы (композиция)
class Car {
private engine: Engine;
private wheels: Wheels;
constructor() {
this.engine = new Engine();
this.wheels = new Wheels();
}
drive(): void {
this.engine.start();
this.wheels.rotate();
console.log("Машина поехала!");
}
}
const myCar = new Car();
myCar.drive();
// Двигатель запущен
// Колёса крутятся
// Машина поехала!
Сравнение с Python: чем похоже и чем отличается
| Характеристика | TypeScript | Python |
|---|---|---|
| Объявление класса | class Name {} |
class Name: |
| Конструктор | constructor() |
def __init__(self) |
| Ссылка на текущий объект | this |
self |
| Типизация свойств | Обязательна: name: string |
Необязательна (динамическая) |
| Приватные поля | private (на уровне языка) |
__name (соглашение, не строгое) |
| Геттеры/сеттеры | get/set |
@property |
| Статические методы | static method() |
@staticmethod |
| Абстрактные классы | abstract class |
ABC из модуля abc |
| Интерфейсы | Есть (interface) |
Нет (используются абстрактные классы) |
💡 Главное отличие: TypeScript — статически типизированный язык. Ты обязан указать типы свойств и параметров. Python — динамический, типы проверяются во время выполнения. TypeScript компилируется в JavaScript, Python — интерпретируемый.
Типичные ошибки и подводные камни
-
Забыть вызвать super() в наследнике
class Parent { constructor(public name: string) {} } class Child extends Parent { constructor(name: string, public age: number) { // ❌ Забыли super(name) } }TypeScript выдаст ошибку: Constructors for derived classes must contain a 'super' call.
-
Путаница между this и super
this— ссылается на текущий объект.super— ссылается на родительский класс. Нельзя использоватьthisдо вызоваsuper()в конструкторе наследника. -
Изменение приватного поля напрямую
class User { private password: string = "123"; } const user = new User(); // user.password = "456"; // ❌ Ошибка: password — privateИспользуй геттеры/сеттеры или публичные методы для доступа.
-
Забыть указать тип свойства
class Item { name; // ❌ Не указан тип — будет any }Всегда указывай тип, иначе теряется смысл использования TypeScript.
-
Создание объекта без new
const obj = MyClass(); // ❌ Ошибка: нельзя вызвать класс как функциюВсегда используй
new. -
Переопределение метода с неправильной сигнатурой
class Base { greet(name: string): void {} } class Derived extends Base { // ❌ Параметр другого типа greet(name: number): void {} }TypeScript не разрешит изменить типы параметров при переопределении.
-
Использование protected там, где нужно private
protected— доступно наследникам. Если наследникам не нужно видеть поле, используйprivate.
Лучшие практики и советы профессионалов
- Принцип единственной ответственности. Один класс — одна задача. Не делай класс «Бог», который умеет всё. Разделяй на маленькие классы.
- Используй сокращённую запись свойств в конструкторе. Это уменьшает количество кода и делает его чище.
- Делай поля приватными по умолчанию. Открывай доступ только через методы или геттеры. Это защищает данные.
- Предпочитай композицию наследованию. Вместо «Автомобиль наследует Двигатель» лучше «Автомобиль содержит Двигатель».
- Используй интерфейсы для описания контрактов. Это делает код гибким и тестируемым.
- Не злоупотребляй статическими методами. Они удобны для утилит, но усложняют тестирование, если используются для хранения состояния.
- Всегда указывай возвращаемый тип методов. Даже если это
void. Это улучшает читаемость. - Используй readonly для неизменяемых полей. Если поле не должно меняться после создания объекта, пометь его
readonly.
🎯 Совет: Думай о классах как о «чертежах реальных сущностей». Если ты описываешь «Студента», у него должны быть свойства (имя, возраст, курс) и методы (учиться, сдавать экзамен). Не добавляй в класс «Студент» методы для управления базой данных — это задача другого класса.
Вопросы для самопроверки
-
Вопрос: В чём разница между классом и объектом?
Ответ: Класс — это чертёж (шаблон), который описывает структуру и поведение. Объект — это конкретный экземпляр, созданный по этому чертежу. Класс один, объектов может быть много.
-
Вопрос: Что такое конструктор и зачем он нужен?
Ответ: Конструктор — это специальный метод, который вызывается автоматически при создании объекта через
new. Он нужен для инициализации начальных значений свойств объекта. -
Вопрос: Какие модификаторы доступа есть в TypeScript?
Ответ:
public(доступно всем),private(только внутри класса),protected(внутри класса и его наследников). -
Вопрос: Чем отличается статический метод от обычного?
Ответ: Статический метод принадлежит классу, а не объекту. Его вызывают через имя класса (например,
MathHelper.circleArea()), без создания экземпляра. -
Вопрос: Что такое наследование и зачем оно нужно?
Ответ: Наследование позволяет создать новый класс на основе существующего. Дочерний класс получает все свойства и методы родителя и может добавлять свои или переопределять существующие. Это уменьшает дублирование кода.
-
Вопрос: Что произойдёт, если в дочернем классе не вызвать
super()?Ответ: TypeScript выдаст ошибку компиляции. Конструктор дочернего класса обязан вызвать конструктор родителя через
super(). -
Вопрос: Чем абстрактный класс отличается от обычного?
Ответ: Абстрактный класс нельзя использовать для создания объектов напрямую. Он служит только базой для наследования. В нём могут быть абстрактные методы (без реализации), которые обязаны реализовать наследники.
-
Вопрос: Что такое интерфейс и как он связан с классом?
Ответ: Интерфейс описывает структуру (какие свойства и методы должны быть), но не содержит реализации. Класс может реализовать интерфейс через ключевое слово
implements, и тогда он обязан предоставить реализацию всех методов интерфейса. -
Вопрос: В чём разница между
public readonly name: stringиprivate name: string?Ответ:
readonlyозначает, что свойство можно прочитать, но нельзя изменить после инициализации.privateозначает, что свойство нельзя прочитать или изменить снаружи класса. Можно комбинировать:private readonly. -
Вопрос: Почему не стоит делать все поля публичными?
Ответ: Публичные поля нарушают инкапсуляцию — любой код может изменить их напрямую, что может привести к некорректному состоянию объекта. Лучше контролировать доступ через методы и геттеры/сеттеры.
Итог и что изучить дальше
Мы разобрали фундамент объектно-ориентированного программирования в TypeScript. Ты узнал:
- Что класс — это чертёж, а объект — конкретная реализация.
- Как объявлять классы, свойства, методы, конструкторы.
- Как работают модификаторы доступа, наследование, абстрактные классы и интерфейсы.
- Как избегать типичных ошибок и писать чистый код.
Теперь, когда ты освоил базу, вот что стоит изучить дальше:
- Дженерики (Generics) — создание классов и функций, которые работают с разными типами данных.
- Декораторы — мощный инструмент для изменения поведения классов (используется в Angular).
- SOLID — пять принципов объектно-ориентированного проектирования, которые помогут писать поддерживаемый код.
- Паттерны проектирования — готовые решения типовых задач (Одиночка, Фабрика, Стратегия и другие).
- TypeScript с фреймворками — попробуй применить знания в React, Angular или NestJS.
🚀 Напутствие: Не пытайся выучить всё сразу. Напиши несколько своих классов: «Студент», «Автомобиль», «Заказ в интернет-магазине». Поэкспериментируй с наследованием и интерфейсами. Лучший способ понять ООП — писать код каждый день.