Статические и приватные поля

прототипы≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

У класса есть не только экземпляры. Часть данных и поведения принадлежит самому классу — за это отвечает static, а часть не должна покидать его тело вовсе — для этого есть поля с #.

Разделы темы:·

Обычные методы живут на прототипе и достаются каждому объекту. Но часть поведения относится не к отдельному экземпляру, а ко всему виду сразу: общая константа, счётчик созданных объектов, проверка строки ещё до того, как объект появился. Слово static кладёт поле или метод на сам класс.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вторая строка вывода — главное, что стоит запомнить: у экземпляра статического метода нет. Статика не спускается вниз, к объектам, зато передаётся вбок — подклассы её наследуют.

Статическое поле тоже одно на весь класс, а не по копии на объект. Отсюда классический приём — счётчик, который увеличивается в конструкторе.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

У конструктора одно имя и один набор аргументов. Как только объект можно собрать несколькими способами — из массива, из строки, из рублей, — конструктор начинает обрастать проверками «а что мне передали на этот раз». Статическая фабрика убирает эту развилку: это обычный статический метод, который сам зовёт new и возвращает готовый объект.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Выигрыш не только в аккуратности, но и в читаемости на месте вызова. Money.fromRubles(2.5) понятен без документации, а new Money(2.5) заставляет открывать конструктор и гадать, рубли там или копейки.

Поэтому в фабрике часто пишут new this() вместо явного имени класса: подкласс унаследует фабрику и будет создавать объекты своего типа, а не родительского.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Поле, имя которого начинается с #, существует только внутри тела класса. Снаружи к нему не подобраться никак: ни точкой, ни через квадратные скобки, ни перебором ключей, ни через JSON.stringify. Объявить такое поле нужно прямо в теле класса — просто присвоить this.#value в конструкторе, не объявив поле, не получится.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Приватность здесь синтаксическая, а не по договорённости. Если написать c.#value за пределами класса, движок откажется разбирать сам файл: ошибка прилетит ещё до запуска, а не в момент вызова.

# — часть имени поля, а не модификатор доступа. Поэтому #name и name спокойно уживаются в одном классе как два разных поля, а пробел после решётки — синтаксическая ошибка.

Каждое публичное поле — это обещание. Пока balance торчит наружу, любой участок программы может записать туда минус, строку или undefined, и класс перестаёт отвечать за собственную корректность. Спрятав поле, вы оставляете снаружи только те действия, которые не способны привести объект в невозможное состояние.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Второй выигрыш — свобода переписывать. Внутреннее устройство не входит в публичный договор: пока deposit и геттер balance ведут себя как раньше, можно хранить сумму хоть в копейках, хоть в отдельном объекте — снаружи никто ничего не заметит.

До появления # приватность была вопросом вежливости: поле называли _token, и это значило «сюда не лезь». Беда в том, что язык о таком соглашении не знает — поле остаётся обычным и ведёт себя как обычное.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Что происходит_token#token
Чтение и запись снаружиработаютSyntaxError ещё до запуска
Виден в Object.keys и JSON.stringifyданет
Поле с тем же именем в подклассеперетирает родительскоенезависимо от родительского
Гарантияобещание разработчикаправило языка

Решётка работает и со статикой: static #instances = 0 — общий на класс счётчик, к которому не дотянуться извне. Комбинация «приватная статика плюс статическая фабрика» даёт классический одиночка.

Экземпляр хранится в приватном статическом поле, а наружу выставлен единственный вход — фабрика. Подменить или обнулить хранилище со стороны невозможно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • static кладёт поле или метод на сам класс — у экземпляров их нет, зато подклассы наследуют.
  • Статическая фабрика — это именованный конструктор: Money.fromRubles(2.5) честнее, чем new Money(2.5).
  • Поле с # доступно только внутри тела класса; обращение снаружи — SyntaxError ещё до запуска кода.
  • Приватные поля не видны ни в Object.keys, ни в JSON.stringify, и не конфликтуют с полями подклассов.
  • _name — просьба, #name — правило языка; статика тоже бывает приватной: static #instance.

1/3В классе A объявлено static n = 5. Что выведет console.log(new A().n)?

2/3Что произойдёт, если обратиться к приватному полю снаружи класса: console.log(obj.#secret)?

3/3class P { #x = 1; y = 2; } — что выведет JSON.stringify(new P())?

Билеты с номерами

Напишите класс Ticket. Номер билета хранится в приватном поле и доступен снаружи только на чтение — через геттер number. Сколько билетов уже выдано, класс считает сам: в приватном статическом поле.

Создавать билеты напрямую через new не нужно — сделайте статическую фабрику next(), которая выдаёт следующий по порядку билет. Первый вызов должен дать номер 1, второй — 2. Проверьте заодно, что Object.keys у готового билета возвращает пустой массив.

Счётчик и экземпляр связывает только фабрика: снаружи нет ни доступа к счётчику, ни возможности переписать номер.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Поле #number объявлено в теле класса без значения — так тоже можно, важно лишь само объявление. Геттер без сеттера делает номер доступным только на чтение.

Из подчёркивания в решётку

Есть класс термометра, где температура лежит в поле _celsius. Любой код снаружи может записать туда −500 градусов, что физически невозможно.

Перепишите класс: сделайте поле приватным, добавьте геттер celsius и метод set(value), который молча игнорирует значения ниже абсолютного нуля (−273.15). После set(25) и set(-300) геттер должен вернуть 25.

Вся защита держится на том, что записать значение можно только через метод — прямого пути к полю больше нет.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

С _celsius такой гарантии не было бы: соглашение об именовании ничего не запрещает, оно лишь просит.