JavaScript / 38 минут

Прототипы и классы

Поймите прототипную цепочку, методы классов и границы инкапсуляции.

В процессе Теория: впереди Практика: впереди Тест: впереди
12 / 22
После этой главы
  • Поймите прототипную цепочку, методы классов и границы инкапсуляции.
  • Применить тему в задаче «Прототипы и классы» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Свойства экземпляра и цепочка прототипов

Объект может получать свойства не только из собственного набора, но и через прототипную цепочку. Если собственного свойства нет, поиск продолжается у прототипа, затем у его прототипа и так далее до конца цепочки. Это механизм делегирования поведения, а не обязательное копирование всех методов в каждый экземпляр. Проверка Object.hasOwn помогает отличить собственные данные от унаследованного доступа.

Класс предоставляет удобный синтаксис создания объектов с общим поведением. Он не отменяет прототипную модель JavaScript. Методы, объявленные в теле класса обычным синтаксисом метода, обычно находятся в prototype и доступны экземплярам через цепочку. Поля экземпляра, напротив, принадлежат каждому созданному объекту отдельно.

JavaScript
class Learner {
  constructor(name) { this.name = name; }
  greeting() { return `Привет, ${this.name}`; }
}
const learner = new Learner('Анна');
console.log(Object.hasOwn(learner, 'name')); // true
console.log(Object.hasOwn(learner, 'greeting')); // false

Конструктор и состояние объекта

new создаёт экземпляр, связывает его с прототипом и запускает constructor с подходящим this. Конструктор задаёт начальное корректное состояние. Если объект не имеет смысла без имени или положительного лимита, это правило полезно выразить при создании, а не ждать случайного сбоя через несколько методов. Однако внешнюю runtime-валидацию ввода всё равно проводят на границе приложения.

JavaScript
class Wallet {
  constructor(balance = 0) { this.balance = balance; }
  deposit(amount) {
    if (amount > 0) this.balance += amount;
  }
}
const wallet = new Wallet(10);
wallet.deposit(5);
console.log(wallet.balance); // 15

Метод deposit изменяет конкретный экземпляр через this.balance. Второй кошелёк не должен использовать те же данные случайно: начальное состояние создаётся для каждого new. Если хранить balance как глобальную переменную модуля, все экземпляры потеряют независимость. Выбирайте расположение состояния исходя из того, кому оно принадлежит и как долго должно жить.

Публичные и приватные поля

Обычное поле доступно внешнему коду. Поле с # имеет специальную runtime-семантику приватности: произвольное чтение снаружи класса не разрешено. Это отличается от соглашения называть поле _balance и от статического TypeScript private. Публичный метод может предоставлять ограниченную операцию над приватным состоянием, сохраняя инварианты объекта.

JavaScript
class Counter {
  #value = 0;
  increment() { this.#value += 1; }
  get value() { return this.#value; }
}
const counter = new Counter();
counter.increment();
console.log(counter.value); // 1

Приватное поле не делает объект подходящим хранилищем серверного секрета в опубликованном клиентском коде. Код приложения и данные, переданные клиенту, находятся под контролем пользователя его среды. Инкапсуляция помогает управлять изменениями внутри программы, а реальная граница доверия между клиентом и сервером требует серверной проверки.

Наследование и super

extends связывает классы отношением наследования. Подкласс может переиспользовать поведение и переопределять методы. В конструкторе подкласса нужно вызвать super до обращения к this. Используйте наследование, когда отношение действительно соответствует взаимозаменяемости контрактов, а не только потому, что два класса имеют несколько похожих строк.

JavaScript
class Message {
  constructor(text) { this.text = text; }
  render() { return this.text; }
}
class ImportantMessage extends Message {
  render() { return `Важно: ${super.render()}`; }
}

Большая иерархия делает изменения связанных классов дорогими. Часто полезнее композиция: объект получает отдельную зависимость для хранения, форматирования или отправки и использует её контракт. Позже этот же принцип будет важен в Express-сервисах и DI NestJS. Общий код сам по себе не является достаточным основанием для общего предка.

Статические методы и контекст вызова

static-метод принадлежит самому классу, а не экземпляру. Он удобен для операции, которая не использует состояние конкретного объекта, например фабрики из DTO. Не путайте класс Account и объект account: доступные у них свойства различаются. Метод экземпляра, переданный отдельным callback, может потерять нужный this так же, как метод обычного объекта.

Выбор между классом, замыканием и набором чистых функций определяется моделью задачи. Класс полезен при явной сущности с состоянием и поведением; замыкание удобно для небольшой закрытой фабрики; чистая функция часто лучше для расчёта без собственного жизненного цикла. Не используйте классы как обязательную обёртку над каждой строкой кода.

Практическая модель счёта

В задании создаётся Account с начальным балансом. Каждая положительная сумма увеличивает его, ноль и отрицательные значения игнорируются по условию. Это учебное правило конкретного метода deposit, а не универсальная банковская модель. Реальное списание и зачисление потребовало бы отдельных доменных операций и правил точности денег.

Проверьте отсутствие операций, одну положительную сумму и смесь знаков. Создайте новый экземпляр при каждом solve. После решения объясните, где хранится balance, почему метод общий для экземпляров, но данные раздельные, и как изменился бы публичный контракт при переходе на приватное #balance с getter. Важно сохранить поведение, а не просто заменить видимость поля.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0