Классы и конструкторы
Объявление классов, конструкторы, свойства и методы в TypeScript
Цели урока
- Понять, что такое классы и зачем они нужны в программировании
- Научиться объявлять классы, добавлять свойства и методы
- Освоить работу конструкторов и сокращённый синтаксис параметров
- Понять модификаторы доступа: public, private, protected, readonly
- Научиться создавать экземпляры классов и работать с ними
- Разобрать практические примеры: User, BankAccount, Rectangle
- Узнать типичные ошибки новичков и как их избежать
1. Что такое классы — аналогия и определение
Давайте начнём с самого главного вопроса: зачем вообще нужны классы?
Представьте, что вы работаете на конфетной фабрике. У вас есть огромная форма (литьевая форма), в которую заливается шоколад, и на выходе получаются одинаковые конфеты. Одна форма — много конфет. Каждая конфета имеет свою обёртку, свой вес, свой вкус, но все они сделаны по одному шаблону.
Класс — это форма для конфет (шаблон, чертёж). Объект — это сама конфета, которая получилась из этой формы. Из одной формы можно сделать сколько угодно конфет, и каждая будет иметь свои конкретные значения (начинку, вес, цвет обёртки), но структура у всех одна и та же.
В программировании класс — это шаблон для создания объектов. Он описывает:
- Какие данные будет хранить объект (свойства / поля)
- Что умеет делать объект (методы / функции)
- Как создаётся объект (конструктор)
Ещё одна аналогия: класс — это чертёж дома. На чертеже написано: есть стены, есть крыша, есть двери, есть окна. Сам по себе чертёж не является домом — в нём нельзя жить. Но по этому чертежу можно построить сколько угодно домов, и каждый дом будет иметь свои обои, свою мебель, своих жильцов.
Без классов нам пришлось бы для каждого объекта описывать его свойства и функции отдельно. Это работало бы, но быстро превратилось бы в хаос. Классы позволяют:
- Группировать связанные данные и функции в одном месте
- Создавать много одинаковых объектов из одного шаблона
- Изменять код в одном месте, и это применится ко всем объектам
- Делать код более понятным и структурированным
В TypeScript классы объединяют в себе два мира: значения (JavaScript-классы, которые можно использовать как в рантайме) и типы (интерфейсы, описывающие структуру класса). Это делает TypeScript особенно мощным для работы с классами.
2. Объявление класса — синтаксис и правила именования
Самый простой класс выглядит так:
}
Это пустой класс — без свойств, без методов, без конструктора. Он не очень полезен, но он уже рабочий. Разберём синтаксис:
class— ключевое слово, которое говорит «я объявляю класс»Dog— имя класса{ }— тело класса, где живут свойства и методы
Имена классов в TypeScript (и JavaScript) принято писать в PascalCase — каждое слово с заглавной буквы, без пробелов и подчёркиваний. Например: Dog, BankAccount, UserProfile, ShoppingCart. Это не синтаксическая ошибка написать myClass или my_class, но это нарушение конвенции, и опытные разработчики сразу заметят.
Давайте объявим более осмысленный класс с комментариями:
class Book {
title: string;
author: string;
pages: number;
}
Этот класс описывает книгу с тремя свойствами: название (title), автор (author) и количество страниц (pages). Пока что мы только описали структуру — сам по себе класс Book не является какой-то конкретной книгой. Он лишь говорит: «вот что значит быть книгой».
В TypeScript свойства класса объявляются прямо в теле класса с указанием типа. Это отличие от JavaScript, где свойства часто добавляются только в конструкторе. TypeScript позволяет (и даже требует) объявлять типы свойств заранее.
Класс без тела тожедопустимыйен, но бесполезен:
В реальной практике такой класс бесполезен, но он не вызывает ошибку. TypeScript позволяет создавать пустые классы — возможно, вы будете добавлять свойства позже.
3. Свойства класса (поля) — объявление и типизация
Свойства (поля) — это переменные, которые принадлежат классу. Они описывают состояние объекта. Если класс — это чертёж дома, то свойства — это «количество комнат», «этажность», «наличие гаража».
name: string;
age: number;
isEnrolled: boolean;
}
Здесь у нас три свойства:
name— строка, имя студентаage— число, возраст студентаisEnrolled— булево значение, записан ли студент
Можно задавать начальные значения (дефолтные значения) сразу:
theme: string = "light";
fontSize: number = 14;
notifications: boolean = true;
}
Теперь, когда мы создадим объект класса Settings, его свойства уже будут иметь значения по умолчанию: тема «light», размер шрифта 14, уведомления включены.
Свойства могут быть различными типами, включая массивы и объекты:
name: string;
tracks: string[];
duration: number;
isPublic: boolean;
}
Всегда указывайте типы для свойств класса. TypeScript может вывести тип автоматически, но явная типизация делает код более понятным и помогает отловить ошибки на ранней стадии. Никогда не пишите просто name; без типа — это вызовет ошибку.
Также свойства могут иметь типы-ссылки на другие типы:
street: string;
city: string;
};
class Person {
name: string;
address: Address;
}
4. Конструктор — зачем нужен и как работает
Конструктор — это специальный метод, который вызывается автоматически при создании нового объекта (экземпляра) класса. Его главная задача — задать начальные значения свойствам.
Конструктор — это как инструкция по сборке мебели из IKEA. Когда вы покупаете шкаф, он приходит в коробке. Конструктор — это тот процесс, где из коробки получается готовый шкаф с определёнными параметрами: цвет, размер, количество полок.
Синтаксис конструктора:
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
Разберём по частям:
constructor— ключевое слово, обозначающее конструктор(name: string, email: string)— параметры конструктораthis.name = name— присваиваем значение параметра свойству класса
Здесь возникает важный вопрос: что такое this?
this — это ссылка на текущий объект, то есть на тот экземпляр класса, который создаётся или уже существует. Когда мы пишем this.name, мы говорим: «возьми свойство name текущего объекта».
Параметр конструктора name и свойство класса name — это две разные переменные. Они могут иметь одно имя, но это не одно и то же. this.name — это свойство объекта, а просто name — это значение параметра. Без this TypeScript не сможет понять, о чём идёт речь.
Теперь, когда мы создаём объект, конструктор автоматически вызывается:
const user1 = new User("Алексей", "alex@mail.ru");
// TypeScript автоматически вызывает конструктор
// конструктор выполняет: this.name = "Алексей"; this.email = "alex@mail.ru";
console.log(user1.name); // "Алексей"
console.log(user1.email); // "alex@mail.ru"
Ключевое слово new создаёт новый объект и запускает конструктор. Без new класс не может породить объект.
Конструктор может иметь любое количество параметров — от нуля до скольки угодно:
class Color {
r: number;
g: number;
b: number;
constructor() {
this.r = 0;
this.g = 0;
this.b = 0;
}
}
// Конструктор с одним параметром
class Counter {
value: number;
constructor(startValue: number) {
this.value = startValue;
}
}
// Конструктор с тремя параметрами
class Product {
name: string;
price: number;
category: string;
constructor(name: string, price: number, category: string) {
this.name = name;
this.price = price;
this.category = category;
}
}
Конструктор может быть только один в классе. JavaScript (и TypeScript) не поддерживают перегрузку конструкторов как, например, Java. Если вам нужно создавать объекты по-разному, используйте параметры с значениями по умолчанию или статические методы.
Параметры конструктора могут иметь значения по умолчанию:
brand: string;
year: number;
constructor(brand: string, year: number = 2024) {
this.brand = brand;
this.year = year;
}
}
const car1 = new Car("Toyota"); // year = 2024 по умолчанию
const car2 = new Car("BMW", 2020); // year = 2020
5. Методы класса — функции внутри класса
Методы — это функции, которые принадлежат классу. Если свойства описывают состояние объекта (что он имеет), то методы описывают поведение (что он умеет делать).
Свойства — это «что есть у человека» (имя, возраст, рост). Методы — это «что человек умеет делать» (ходить, говорить, есть). Класс описывает и то, и другое вместе.
Синтаксис метода:
result: number = 0;
add(value: number): void {
this.result += value;
}
subtract(value: number): void {
this.result -= value;
}
getResult(): number {
return this.result;
}
}
Здесь:
add(value: number): void— метод, который принимает число и ничего не возвращает (void)subtract(value: number): void— аналогичноgetResult(): number— метод, который возвращает число
Использование:
calc.add(10);
calc.add(5);
calc.subtract(3);
console.log(calc.getResult()); // 12
Методы могут обращаться к свойствам через this и вызывать другие методы:
name: string;
constructor(name: string) {
this.name = name;
}
getGreeting(): string {
return `Привет, ${this.name}!`;
}
logGreeting(): void {
const message = this.getGreeting();
console.log(message);
}
}
Методы также могут принимать необязательные параметры:
log(message: string, prefix?: string): void {
const formattedMessage = prefix
? `[${prefix}] ${message}`
: message;
console.log(formattedMessage);
}
}
const logger = new Logger();
logger.log("Загрузка завершена"); // "Загрузка завершена"
logger.log("Загрузка завершена", "INFO"); // "[INFO] Загрузка завершена"
Методы класса не нуждаются в ключевом слове function. Они объявляются просто как имяМетода() { }. Это особенность синтаксиса классов.
6. Создание экземпляров — new ClassName()
Экземпляр (instance) — это конкретный объект, созданный по шаблону класса. Мы уже видели примеры выше, но давайте разберём подробнее.
name: string;
color: string;
constructor(name: string, color: string) {
this.name = name;
this.color = color;
}
}
// Создаём три экземпляра одного класса
const apple = new Fruit("Яблоко", "красное");
const banana = new Fruit("Банан", "жёлтый");
const grape = new Fruit("Виноград", "фиолетовый");
// Каждый экземпляр — независимый объект
console.log(apple.name); // "Яблоко"
console.log(banana.color); // "жёлтый"
console.log(grape.name); // "Виноград"
Каждый вызов new Fruit(...) создаёт новый независимый объект. Изменение одного объекта не влияет на другие:
class Fruit {
name: string;
color: string;
constructor(name: string, color: string) {
this.name = name;
this.color = color;
}
describe(): string {
return `${this.name} — ${this.color}`;
}
}
const apple = new Fruit("Яблоко", "красное");
const banana = new Fruit("Банан", "жёлтый");
console.log(apple.describe()); // "Яблоко — красное"
console.log(banana.describe()); // "Банан — жёлтый"
Мы можем создавать переменные с типом класса:
const apple: Fruit = new Fruit("Яблоко", "красное");
// TypeScript выведет тип автоматически
const banana = new Fruit("Банан", "жёлтый");
В большинстве случаев TypeScript сам выведет тип экземпляра. Указывать тип явно (const apple: Fruit = new Fruit(...)) нужно только когда это действительно помогает — например, при объявлении переменной без инициализации.
7. Сокращённый синтаксис конструктора (parameter properties)
TypeScript предлагает удивительно удобную фичу, которая экономит много кода. Вместо того чтобы объявлять свойства отдельно и затем копировать их в конструкторе, можно сделать всё за одну строку!
Сначала посмотрим на «длинный» способ, который мы уже знаем:
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
А теперь — короткий способ с помощью модификаторов в параметрах конструктора:
class User {
constructor(public name: string, public age: number) {}
}
Это делает ровно то же самое! TypeScript автоматически:
- Объявляет свойства
nameиageв классе - Генерирует код
this.name = name; this.age = age;
Давайте сравним:
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// Короткий способ (3 строки)
class User {
constructor(public name: string, public age: number) {}
}
Модификаторы, которые можно использовать в параметрах конструктора:
| public | Свойство доступно везде (по умолчанию) |
| private | Свойство доступно только внутри класса |
| protected | Свойство доступно внутри класса и его наследников |
| readonly | Свойство можно задать только один раз (в конструкторе) |
Пример с разными модификаторами:
constructor(
public name: string, // доступно всем
private salary: number, // только внутри класса
protected department: string, // класс и наследники
readonly id: number // нельзя изменить
) {}
}
Сокращённый синтаксис конструктора — это стандарт де-факто в TypeScript. Используйте его каждый раз, когда нужно просто скопировать параметры в свойства. Длинный способ оставьте для случаев, когда в конструкторе есть дополнительная логика.
8. Модификаторы доступа — public, private, protected, readonly
Модификаторы доступа — это правила видимости. Они определяют, кто может «видеть» и «использовать» свойства и методы класса.
Представьте квартиру в многоквартирном доме. public — это лестница, доступная всем. private — это ваша комната, куда никто другой не может зайти. protected — это ваша квартира, куда могут зайти только члены вашей семьи (наследники).
public — доступно всем
constructor(
public brand: string,
public color: string
) {}
}
const car = new Car("Toyota", "белый");
console.log(car.brand); // "Toyota" ✓
car.color = "чёрный"; // ✓ можно изменить
private — доступно только внутри класса
constructor(
public owner: string,
private balance: number
) {}
deposit(amount: number): void {
this.balance += amount;
}
getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("Иван", 1000);
console.log(account.owner); // "Иван" ✓
console.log(account.balance); // ✗ Ошибка: Property 'balance' is private
account.balance = 999999; // ✗ Ошибка: Property 'balance' is private
account.deposit(500); // ✓ Через метод можно
console.log(account.getBalance()); // 1500
Приватные свойства защищают данные от прямого доступа. Это важно для безопасности: вы не хотите, чтобы кто-то мог бесконтрольно менять баланс банковского счёта.
protected — доступно классу и его наследникам
constructor(
public name: string,
protected energy: number
) {}
eat(food: number): void {
this.energy += food;
}
}
// Dog — наследник Animal
class Dog extends Animal {
fetch(): void {
// Dog может обращаться к protected свойству
this.energy -= 10;
}
}
const dog = new Dog("Шарик", 100);
console.log(dog.name); // "Шарик" ✓
console.log(dog.energy); // ✗ Ошибка: Property 'energy' is protected
readonly — только для чтения
constructor(
public readonly version: string,
public name: string
) {}
}
const config = new Config("1.0.0", "MyApp");
console.log(config.version); // "1.0.0" ✓
config.version = "2.0.0"; // ✗ Ошибка: Cannot assign to 'version' because it is a read-only property
config.name = "NewApp"; // ✓ Обычное свойство можно менять
readonly работает как во время выполнения, так и на уровне типов. Даже если вы попытаетесь изменить readonly свойство через JavaScript-трюки, TypeScript покажет ошибку. Однако в рантайме (обычный JS) readonly свойства всё же можно изменить — TypeScript проверяет только на этапе компиляции.
9. Метод toString() — красивое представление объекта
Метод toString() возвращает строковое представление объекта. По умолчанию он возвращает что-то вроде [object Object], что не очень информативно. Мы можем переопределить его:
constructor(
public name: string,
public age: number
) {}
toString(): string {
return `Person(name: ${this.name}, age: ${this.age})`;
}
}
const person = new Person("Анна", 25);
console.log(person.toString()); // "Person(name: Анна, age: 25)"
// toString() вызывается автоматически при преобразовании в строку:
console.log("Пользователь: " + person); // "Пользователь: Person(name: Анна, age: 25)"
Метод toString() автоматически вызывается при:
- Конкатенации строки с объектом:
"User: " + obj - Вызове
String(obj) - Вызове
obj.toString()вручную - При использовании шаблонных строк:
`${obj}`
Всегда переопределяйте toString() в классах, которые будут использоваться в логировании или отладке. Это значительно упрощает отслеживание проблем.
10. Типизация this в методах
TypeScript обычно автоматически выводит тип this в методах класса. Но иногда возникают ситуации, когда нужно указать тип явно.
constructor(
public x: number,
public y: number
) {}
// TypeScript автоматически определяет тип this как Point
distanceTo(other: Point): number {
// this имеет тип Point, поэтому this.x и this.y — числа
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Явная типизация this — полезно для цепочки вызовов
moveX(delta: number): this {
this.x += delta;
return this;
}
moveY(delta: number): this {
this.y += delta;
return this;
}
}
// Благодаря типу this, можно строить цепочки вызовов:
const point = new Point(0, 0);
point.moveX(5).moveY(3);
console.log(point.x, point.y); // 5 3
Тип this как возвращаемое значение полезен для паттерна Fluent Interface (цепочки вызовов). Когда метод возвращает this, можно вызывать следующий метод сразу:
point.moveX(5);
point.moveY(3);
// С возвратом this — компактно и читаемо:
point.moveX(5).moveY(3);
11. Практические примеры
Пример 1: Класс User
login: string;
email: string;
age: number;
isVerified: boolean = false;
constructor(
login: string,
email: string,
age: number
) {
this.login = login;
this.email = email;
this.age = age;
}
verify(): void {
this.isVerified = true;
console.log(`Пользователь ${this.login} верифицирован!`);
}
isAdult(): boolean {
return this.age >= 18;
}
toString(): string {
return `User(${this.login}, ${this.age}, verified: ${this.isVerified})`;
}
}
const user1 = new User("coder42", "coder@mail.ru", 22);
user1.verify(); // "Пользователь coder42 верифицирован!"
console.log(user1.isAdult()); // true
console.log(user1.toString()); // "User(coder42, 22, verified: true)"
Пример 2: Класс BankAccount
private balance: number;
private history: string[] = [];
constructor(
public owner: string,
initialBalance: number = 0
) {
if (initialBalance < 0) {
throw new Error("Начальный баланс не может быть отрицательным");
}
this.balance = initialBalance;
this.history.push(`Открыт счёт: ${initialBalance} руб.`);
}
deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Сумма пополнения должна быть положительной");
}
this.balance += amount;
this.history.push(`Пополнение: +${amount} руб.`);
}
withdraw(amount: number): void {
if (amount > this.balance) {
throw new Error("Недостаточно средств");
}
this.balance -= amount;
this.history.push(`Снятие: -${amount} руб.`);
}
getBalance(): number {
return this.balance;
}
getHistory(): string[] {
return [...this.history];
}
toString(): string {
return `Счёт ${this.owner}: ${this.balance} руб.`;
}
}
const account = new BankAccount("Иван Иванов", 10000);
account.deposit(5000);
account.withdraw(3000);
console.log(account.getBalance()); // 12000
console.log(account.getHistory());
// ["Открыт счёт: 10000 руб.", "Пополнение: +5000 руб.", "Снятие: -3000 руб."]
Пример 3: Класс Rectangle
constructor(
public readonly width: number,
public readonly height: number
) {
if (width <= 0 || height <= 0) {
throw new Error("Ширина и высота должны быть положительными");
}
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
isSquare(): boolean {
return this.width === this.height;
}
toString(): string {
return `Прямоугольник(${this.width} × ${this.height})`;
}
}
const rect = new Rectangle(10, 5);
console.log(rect.area()); // 50
console.log(rect.perimeter()); // 30
console.log(rect.isSquare()); // false
const square = new Rectangle(7, 7);
console.log(square.isSquare()); // true
12. Частые ошибки при работе с классами
Давайте разберём ошибки, которые чаще всего допускают новички при работе с классами.
Ошибка 1: Забыли объявить тип свойства
class User {
name: string;
}
// ✅ Правильно: задали начальное значение
class User {
name: string = "";
}
// ✅ Или используем сокращённый синтаксис
class User {
constructor(public name: string) {}
}
Ошибка 2: Забыли this перед свойством
class User {
name: string;
constructor(name: string) {
name = name; // ❌ Это не присваивает свойству!
}
}
// ✅ Правильно:
class User {
name: string;
constructor(name: string) {
this.name = name; // ✅ this ссылается на свойство класса
}
}
Ошибка 3: Забыли new при создании объекта
class User {
constructor(public name: string) {}
}
const user = User("Алексей"); // ❌ TypeError: User is not a constructor
// ✅ Правильно:
const user = new User("Алексей"); // ✅
Ошибка 4: Доступ к приватному свойству снаружи
class User {
constructor(
public name: string,
private password: string
) {}
}
const user = new User("Алексей", "secret123");
console.log(user.password); // ❌ Ошибка компиляции
// ✅ Правильно: добавить метод для доступа
class User {
constructor(
public name: string,
private password: string
) {}
checkPassword(input: string): boolean {
return input === this.password;
}
}
Ошибка 5: Попытка изменить readonly свойство
class Product {
constructor(
public readonly id: number,
public name: string
) {}
}
const product = new Product(1, "Ноутбук");
product.id = 2; // ❌ Ошибка компиляции
// ✅ Если нужно менять id, уберите readonly:
class Product {
constructor(
public id: number,
public name: string
) {}
}
Ошибка 6: Использование стрелочных функций как методов
class Timer {
seconds = 0;
// Стрелочная функция создаётся при каждом создании экземпляра
// Это использует больше памяти
tick = (): void => {
this.seconds++;
}
// Обычный метод создаётся один раз на класс
start(): void {
// Здесь this будет зависеть от контекста вызова
setInterval(this.tick, 1000);
}
}
Стрелочные функции как классовые свойства — это отдельная тема. Для начинающих рекомендуется использовать обычные методы. Стрелочные функции могут быть полезны, когда нужно сохранить контекст this, например при передаче метода как колбэка. Но это тема для продвинутых курсов.
Практическое задание
Создайте класс Car со следующими требованиями:
- Свойства:
brand(string),model(string),year(number),mileage(number, по умолчанию 0) - Конструктор принимает brand, model, year. Mileage автоматически = 0
- Метод
drive(km: number): void— увеличивает пробег - Метод
getInfo(): string— возвращает строку "Toyota Camry 2020, пробег: 15000 км" - Метод
isOld(): boolean— возвращает true, если год выпуска старше 2015 - Метод
toString(): string— переопределите для красивого вывода
Используйте сокращённый синтаксис конструктора там, где уместно. Добавьте модификаторы доступа: year — readonly, mileage — private (доступен только через метод drive).
Чек-лист урока
- Класс — шаблон для создания объектов, содержащий свойства и методы
- class ClassName {} — объявление класса, имя пишется в PascalCase
- Свойства — переменные внутри класса, описывающие состояние объекта
- Типизация свойств — обязательно указывайте типы для каждого свойства
- constructor() — специальный метод, вызываемый при создании объекта
- this — ссылка на текущий экземпляр класса
- Методы — функции внутри класса, описывающие поведение объекта
- new ClassName() — создание нового экземпляра класса
- Сокращённый синтаксис — public/private/protected в параметрах конструктора автоматически создаёт свойства
- public — доступно всем (по умолчанию)
- private — доступно только внутри класса
- protected — доступно классу и его наследникам
- readonly — свойство можно задать только один раз (в конструкторе)
- toString() — метод для красивого строкового представления объекта
- Тип this — позволяет строить цепочки вызовов (Fluent Interface)
- Типичные ошибки — забыли this, забыли new, доступ к private, изменение readonly
Тест по классам и конструкторам
8 вопросов