Модификаторы доступа — public, private, protected
Инкапсуляция данных и контроль доступа к свойствам и методам класса
Цели урока
- Понять, зачем нужны модификаторы доступа и что такое инкапсуляция
- Освоить три ключевых модификатора: public, private, protected
- Научиться контролировать доступ к свойствам и методам класса
- Освоить геттеры и сеттеры (get/set) для контролируемого доступа
- Научиться применять инкапсуляцию на практике в реальных классах
- Понять типичные ошибки новичков и как их избежать
1. Зачем нужны модификаторы доступа
В предыдущем уроке мы научились создавать классы, добавлять свойства и методы, писать конструкторы. Но что произойдёт, если любой человек сможет менять любое свойство нашего класса?
Представьте, что у вас есть банковский счёт. Баланс — эточувствительныйное (чувствительное) данные. Вы не хотите, чтобы кто-угодно мог подойти и сказать: «Знаешь что? Давай поставим баланс на миллион». Нет! Баланс должен меняться только через контролируемые операции: пополнение, списание — и каждый такой шаг должен фиксироваться.
Представьте дом с несколькими комнатами. public — это прихожая и гостиная, куда могут зайти все гости. private — это ваша спальня или сейф, куда никто кроме вас не может зайти. protected — это семейные комнаты, куда могут заходить только члены вашей семьи (близкие родственники). Модификаторы доступа — это замки на дверях этих комнат.
Без модификаторов доступа у нас нет способа защитить внутренние данные класса. В JavaScript все свойства объекта доступны снаружи, и можно свободно читать и перезаписывать любое из них. TypeScript эту проблему решает через модификаторы.
Ключевая концепция, связанная с модификаторами — это инкапсуляция. Инкапсуляция означает «сокрытие внутренней реализации». Класс прячет свои «внутренности» и предоставляет наружу только то, что нужно пользователю.
Инкапсуляция защищает данные от неправильного использования. Когда вы прячете баланс банковского счёта за методом withdraw(), вы можете проверить, достаточно ли средств. Если бы баланс был публичным, любой мог бы написать account.balance = 999999 и обойти все проверки.
В TypeScript есть три основных модификатора доступа:
- public — доступно отовсюду (по умолчанию)
- private — доступно только внутри самого класса
- protected — доступно внутри класса и его наследников
Давайте рассмотрим каждый из них подробно.
2. public — доступно всем
public — это модификатор по умолчанию. Если вы не указываете модификатор, свойство или метод автоматически становится public.
Публичные свойства и методы доступны отовсюду: из любого другого класса, из любого файла, из любого места программы.
class Car {
public brand: string;
public model: string;
constructor(brand: string, model: string) {
this.brand = brand;
this.model = model;
}
public start(): void {
console.log(`${this.brand} ${this.model} завёлся!`);
}
}
// Без модификатора — тоже public (по умолчанию)
class Car2 {
brand: string;
model: string;
constructor(brand: string, model: string) {
this.brand = brand;
this.model = model;
}
start(): void { /* тоже public по умолчанию */ }
}
Эти два класса абсолютно идентичны. public — единственное значение модификатора, которое можно не писать явно.
Доступ к публичным свойствам и методам:
// ✅ Читаем публичное свойство
console.log(myCar.brand); // "Toyota"
// ✅ Записываем в публичное свойство
myCar.brand = "BMW";
console.log(myCar.brand); // "BMW"
// ✅ Вызываем публичный метод
myCar.start(); // "BMW Camry завёлся!"
Хотя public можно не писать, многие команды и стили кода (например, Airbnb, Google) рекомендуют писать модификатор явно для ясности кода. Если вы видите public name: string, сразу понятно, что это публичное свойство.
Публичные свойства могут быть изменены кем угодно. Это удобно для данных, которые не являются чувствительными:
class UserProfile {
constructor(
public name: string,
public bio: string,
public avatar: string
) {}
}
// Пользователь может свободно обновлять свой профиль
const profile = new UserProfile("Анна", "Программист", "avatar.png");
profile.bio = "Senior разработчик"; // ✅ Это нормально
3. private — доступно только внутри класса
private — это модификатор, который делает свойство или метод доступным только внутри того же класса. Ни снаружи, ни в других классах, ни даже в наследниках — никто не может прочитать или изменить приватное свойство напрямую.
Private — это ваш личный сейф. Только вы знаете код замка (только методы вашего класса могут обращаться к приватным данным). Даже члены семьи (наследники) не могут его открыть.
private balance: number;
constructor(owner: string, initialBalance: number) {
this.balance = initialBalance;
}
// Методы класса МОГУТ обращаться к private свойствам
deposit(amount: number): void {
this.balance += amount;
console.log(`Баланс после пополнения: ${this.balance} руб.`);
}
withdraw(amount: number): void {
if (amount > this.balance) {
throw new Error("Недостаточно средств");
}
this.balance -= amount;
console.log(`Баланс после снятия: ${this.balance} руб.`);
}
getBalance(): number {
return this.balance;
}
}
Теперь попробуем получить доступ к балансу снаружи:
// ✅ Через метод — можно
account.deposit(5000);
account.withdraw(2000);
console.log(account.getBalance()); // 13000
// ❌ Напрямую — ОШИБКА!
console.log(account.balance); // ❌ Property 'balance' is private
// ❌ Попытка изменить — ОШИБКА!
account.balance = 999999; // ❌ Property 'balance' is private
TypeScript не позволит даже прочитать приватное свойство снаружи. Это не просто «пожелание» — это проверка на этапе компиляции.
Приватные методы — аналогичная история:
// Приватный метод — вспомогательная логика
private hasValidFormat(email: string): boolean {
return email.includes("@") && email.includes(".");
}
private hasNoSpaces(email: string): boolean {
return !email.includes(" ");
}
// Публичный метод — пользователь вызывает только его
validate(email: string): boolean {
return this.hasValidFormat(email) && this.hasNoSpaces(email);
}
}
const validator = new EmailValidator();
console.log(validator.validate("user@mail.ru")); // true
// validator.hasValidFormat("test"); // ❌ Метод приватный
Делайте private всё, что не должно быть доступно снаружи: баланс счёта, пароль, внутренние счётчики, вспомогательные методы, временные буферы. Принцип: «всё по умолчанию private, а public — только если действительно нужно».
Важный момент: private-свойства не наследуются. Дочерний класс не может обращаться к private-свойствам родителя:
private secret: string = "приватные данные";
}
class Child extends Parent {
tryAccess(): void {
console.log(this.secret); // ❌ Property 'secret' is private
// и не доступен в классе Child
}
}
4. protected — доступно классу и наследникам
protected — это «золотая середина» между public и private. Protected-свойства доступны:
- Внутри самого класса — да
- В наследниках (классах, которые используют
extends) — да - Снаружи (из других классов, функций) — нет
Protected — это как семейный архив. Родители (родительский класс) и дети (наследники) могут его читать, но посторонние люди не имеют доступа. Вы доверяете своим «наследникам», но не чужакам.
class Animal {
constructor(
public name: string,
protected energy: number
) {}
eat(food: number): void {
this.energy += food;
console.log(`${this.name} поел. Энергия: ${this.energy}`);
}
}
// Наследник — Dog
class Dog extends Animal {
fetch(): void {
// ✅ Dog МОЖЕТ обращаться к protected energy
this.energy -= 10;
console.log(`${this.name} побегал. Энергия: ${this.energy}`);
}
rest(): void {
// ✅ Может и восстанавливать
this.energy += 20;
console.log(`${this.name} отдохнул. Энергия: ${this.energy}`);
}
}
Использование:
// ✅ public name — доступен всем
console.log(dog.name); // "Шарик"
// ✅ public метод eat — доступен всем
dog.eat(30); // "Шарик поел. Энергия: 130"
// ✅ public метод fetch — доступен всем
dog.fetch(); // "Шарик побегал. Энергия: 120"
// ❌ protected energy — НЕ доступен снаружи
console.log(dog.energy); // ❌ Property 'energy' is protected
dog.energy = 999; // ❌ Property 'energy' is protected
Используйте protected, когда планируете наследование и хотите, чтобы дочерние классы могли обращаться к свойствам, но не хотели бы показывать их внешнему миру. Например: внутреннее состояние персонажа в игре, конфигурация базового класса, счётчики и флаги.
Сравнительная таблица всех трёх модификаторов:
| Модификатор | Класс | Наследник | Снаружи |
| public | ✅ | ✅ | ✅ |
| private | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ❌ |
5. Модификаторы в сокращённом синтаксисе конструктора
Как мы уже видели в предыдущем уроке, TypeScript позволяет комбинировать модификаторы доступа с параметрами конструктора. Это создаёт свойство автоматически:
class User {
public name: string;
private password: string;
constructor(name: string, password: string) {
this.name = name;
this.password = password;
}
}
// Короткий способ — то же самое!
class User2 {
constructor(
public name: string,
private password: string
) {}
}
Пример с тремя разными модификаторами:
constructor(
public name: string, // доступно всем
private salary: number, // только внутри класса
protected department: string, // класс + наследники
public readonly id: number // всем, но изменить нельзя
) {}
getSalary(): number {
return this.salary;
}
raise(amount: number): void {
this.salary += amount;
}
}
Комбинация модификаторов в сокращённом синтаксисе — это стандарт де-факто в TypeScript. Почти все классы используют именно этот подход.
6. Геттеры и сеттеры (get/set) — контролируемый доступ
Что делать, если вы хотите, чтобы свойство было доступно для чтения снаружи, но не для записи? Или хотите выполнять какую-то логику при изменении значения?
Для этого в TypeScript есть геттеры (get) и сеттеры (set) — это специальные методы, которые работают как свойства:
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
// Геттер — позволяет ЧИТАТЬ значение как свойство
get celsius(): number {
return this._celsius;
}
// Геттер — вычисляемое значение
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
// Сеттер — позволяет ИЗМЕНЯТЬ значение с валидацией
set celsius(value: number) {
// Валидация: температура не может быть ниже абсолютного нуля
if (value < -273.15) {
throw new Error("Температура не может быть ниже абсолютного нуля");
}
this._celsius = value;
}
}
Использование:
// Читаем через геттер — работает как свойство!
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
// Записываем через сеттер — с валидацией!
temp.celsius = 100; // ✅ Всё хорошо
console.log(temp.fahrenheit); // 212
// Попытка установить невозможную температуру
temp.celsius = -300; // ❌ Error: Температура не может быть ниже абсолютного нуля
Геттеры и сеттеры выглядят как свойства при использовании (temp.celsius), но по сути это вызовы методов. Это позволяет добавить логику (валидацию, вычисления, логирование) без изменения интерфейса класса.
Ещё пример — геттер для форматированного вывода:
constructor(
private firstName: string,
private lastName: string
) {}
// Геттер для полного имени
get full(): string {
return `${this.firstName} ${this.lastName}`;
}
// Геттер для инициалов
get initials(): string {
return `${this.firstName[0]}.${this.lastName[0]}.`;
}
}
const name = new FullName("Алексей", "Петров");
console.log(name.full); // "Алексей Петров"
console.log(name.initials); // "А.П."
7. Инкапсуляция — зачем прятать данные
Инкапсуляция — один из четырёх столпов объектно-ориентированного программирования (наряду с наследованием, полиморфизмом и абстракцией).
Суть инкапсуляции: объект сам управляет своими данными. Внешний код не может напрямую менять внутреннее состояние — он должен использовать публичный интерфейс (методы) класса.
Почему это важно?
1. Валидация данных: Вы можете проверять данные перед сохранением.
private _value: number;
constructor(value: number) {
this.validateAndSet(value);
}
get value(): number {
return this._value;
}
set value(newValue: number) {
this.validateAndSet(newValue);
}
private validateAndSet(value: number): void {
if (value < 0 || value > 150) {
throw new Error("Возраст должен быть от 0 до 150");
}
this._value = value;
}
}
// ✅ Валидное значение
const age = new Age(25);
age.value = 30; // ✅ Ок
age.value = -5; // ❌ Error: Возраст должен быть от 0 до 150
age.value = 200; // ❌ Error: Возраст должен быть от 0 до 150
2. Сокрытие реализации: Вы можете изменить внутреннюю логику, не меняя интерфейс.
// Сегодня это просто счётчик, завтра может стать UUID
class IdGenerator {
private counter: number = 0;
generate(): number {
return ++this.counter;
}
reset(): void {
this.counter = 0;
}
}
3. Невозможность некорректного состояния: Объект всегда находится в валидном состоянии.
// С инкапсуляцией — это невозможно
class TimeRange {
private _start: number;
private _end: number;
constructor(start: number, end: number) {
if (start >= end) {
throw new Error("Начало должно быть раньше конца");
}
this._start = start;
this._end = end;
}
get start(): number { return this._start; }
get end(): number { return this._end; }
get duration(): number {
return this._end - this._start;
}
}
8. Практические примеры — инкапсуляция в действии
Пример 1: Класс Person с приватным возрастом
private _age: number;
constructor(
public name: string,
age: number
) {
this.setAge(age);
}
get age(): number {
return this._age;
}
set age(value: number) {
this.setAge(value);
}
private setAge(value: number): void {
if (value < 0 || value > 150) {
throw new Error("Некорректный возраст");
}
this._age = value;
}
isAdult(): boolean {
return this._age >= 18;
}
toString(): string {
return `${this.name} (${this._age} лет)`;
}
}
const person = new Person("Иван", 25);
console.log(person.age); // 25 (через геттер)
person.age = 26; // ✅ Через сеттер с валидацией
person.age = -5; // ❌ Error: Некорректный возраст
console.log(person.isAdult()); // true
Пример 2: Класс Settings с приватной конфигурацией
private _theme: "light" | "dark" = "light";
private _fontSize: number = 14;
private _language: string = "ru";
get theme(): "light" | "dark" {
return this._theme;
}
set theme(value: "light" | "dark") {
this._theme = value;
console.log(`Тема изменена на: ${value}`);
}
get fontSize(): number {
return this._fontSize;
}
set fontSize(value: number) {
if (value < 8 || value > 72) {
throw new Error("Размер шрифта должен быть от 8 до 72");
}
this._fontSize = value;
}
get language(): string {
return this._language;
}
set language(value: string) {
this._language = value.toLowerCase();
}
// Приватный метод — внутренняя логика
private applyDefaults(): void {
this._theme = "light";
this._fontSize = 14;
}
reset(): void {
this.applyDefaults();
}
}
const settings = new AppSettings();
settings.theme = "dark"; // ✅ "Тема изменена на: dark"
settings.fontSize = 18; // ✅ Ок
settings.fontSize = 5; // ❌ Error: Размер шрифта должен быть от 8 до 72
settings.language = "EN"; // ✅ Автоматически "en"
settings.reset(); // Сброс к дефолту
Пример 3: Класс Queue (очередь) с инкапсуляцией
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
peek(): T | undefined {
return this.items[0];
}
get size(): number {
return this.items.length;
}
get isEmpty(): boolean {
return this.items.length === 0;
}
}
// Внешний код работает только через публичный интерфейс
// Нельзя случайно получить доступ к items напрямую
const queue = new Queue<string>();
queue.enqueue("Задача 1");
queue.enqueue("Задача 2");
console.log(queue.size); // 2
console.log(queue.dequeue()); // "Задача 1"
console.log(queue.isEmpty); // false
9. Частые ошибки при работе с модификаторами
Ошибка 1: Доступ к private свойству снаружи
class Secret {
private code: string = "1234";
}
const s = new Secret();
console.log(s.code); // ❌ Property 'code' is private
// ✅ Решение: добавить публичный метод
class Secret2 {
private code: string = "1234";
verify(input: string): boolean {
return input === this.code;
}
}
Ошибка 2: Доступ к protected снаружи класса
protected data: string = "секрет";
}
const base = new Base();
console.log(base.data); // ❌ Property 'data' is protected
// ✅ Protected доступен ТОЛЬКО в наследниках
class Child extends Base {
showData(): void {
console.log(this.data); // ✅ Внутри наследника — ок
}
}
Ошибка 3: Забыли указать модификатор
// Это может быть намеренно, но часто это случайная ошибка
class User {
constructor(
name: string, // public по умолчанию
password: string // тоже public?! Опасно!
) {}
}
// ✅ Явно указываем модификаторы
class SafeUser {
constructor(
public name: string,
private password: string
) {}
}
Ошибка 4: Попытка переопределить private метод в наследнике
// В наследнике создаётся НОВЫЙ метод с тем же именем
class Parent {
private helper(): string { return "parent"; }
doWork(): string { return this.helper(); }
}
class Child extends Parent {
// Это НЕ переопределение — это новый метод
private helper(): string { return "child"; }
// doWork() по-прежнему вызывает Parent.helper()
}
const child = new Child();
console.log(child.doWork()); // "parent" — не "child"!
Если вам нужно, чтобы метод можно было переопределить в наследнике, используйте protected вместо private. Это одна из ключевых разниц между ними.
Практическое задание
Создайте класс OnlineStore со следующими требованиями:
- Приватное свойство
products(массив объектов {name, price, stock}) - Приватное свойство
revenue(число, начинается с 0) - Публичный метод
addProduct(name, price, stock)— добавляет товар - Публичный метод
sell(productName, quantity)— продаёт товар (уменьшает stock, увеличивает revenue) - Публичный геттер
totalRevenue— возвращает выручку - Публичный геттер
productCount— возвращает количество товаров - Приватный метод
findProduct(name)— поиск товара по имени - Публичный метод
getLowStockProducts(threshold)— товары с stock ниже порога
Чек-лист урока
- public — модификатор по умолчанию, доступно отовсюду
- private — доступно только внутри класса, не наследуется
- protected — доступно классу и наследникам, не доступно снаружи
- Инкапсуляция — сокрытие внутренней реализации через модификаторы
- Геттер (get) — позволяет читать значение как свойство, можно вычислять
- Сеттер (set) — позволяет изменять значение с валидацией
- Принцип — всё по умолчанию private, public только когда нужно
- Приватные свойства — баланс, пароль, внутренние счётчики
- Геттеры/сеттеры — контролируемый доступ к приватным данным
- Protected vs Private — protected для наследования, private для полной изоляции
- Сокращённый синтаксис — public/private/protected в параметрах конструктора
- Частые ошибки — доступ к private/protected снаружи, забыли модификатор
Тест по модификаторам доступа
8 вопросов