Статические и приватные поля
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У класса есть не только экземпляры. Часть данных и поведения принадлежит самому классу — за это отвечает static, а часть не должна покидать его тело вовсе — для этого есть поля с #.
Обычные методы живут на прототипе и достаются каждому объекту. Но часть поведения относится не к отдельному экземпляру, а ко всему виду сразу: общая константа, счётчик созданных объектов, проверка строки ещё до того, как объект появился. Слово static кладёт поле или метод на сам класс.
Вторая строка вывода — главное, что стоит запомнить: у экземпляра статического метода нет. Статика не спускается вниз, к объектам, зато передаётся вбок — подклассы её наследуют.
Статическое поле тоже одно на весь класс, а не по копии на объект. Отсюда классический приём — счётчик, который увеличивается в конструкторе.
У конструктора одно имя и один набор аргументов. Как только объект можно собрать несколькими способами — из массива, из строки, из рублей, — конструктор начинает обрастать проверками «а что мне передали на этот раз». Статическая фабрика убирает эту развилку: это обычный статический метод, который сам зовёт new и возвращает готовый объект.
Выигрыш не только в аккуратности, но и в читаемости на месте вызова. Money.fromRubles(2.5) понятен без документации, а new Money(2.5) заставляет открывать конструктор и гадать, рубли там или копейки.
Поэтому в фабрике часто пишут new this() вместо явного имени класса: подкласс унаследует фабрику и будет создавать объекты своего типа, а не родительского.
Поле, имя которого начинается с #, существует только внутри тела класса. Снаружи к нему не подобраться никак: ни точкой, ни через квадратные скобки, ни перебором ключей, ни через JSON.stringify. Объявить такое поле нужно прямо в теле класса — просто присвоить this.#value в конструкторе, не объявив поле, не получится.
Приватность здесь синтаксическая, а не по договорённости. Если написать c.#value за пределами класса, движок откажется разбирать сам файл: ошибка прилетит ещё до запуска, а не в момент вызова.
# — часть имени поля, а не модификатор доступа. Поэтому #name и name спокойно уживаются в одном классе как два разных поля, а пробел после решётки — синтаксическая ошибка.
Каждое публичное поле — это обещание. Пока balance торчит наружу, любой участок программы может записать туда минус, строку или undefined, и класс перестаёт отвечать за собственную корректность. Спрятав поле, вы оставляете снаружи только те действия, которые не способны привести объект в невозможное состояние.
Второй выигрыш — свобода переписывать. Внутреннее устройство не входит в публичный договор: пока deposit и геттер balance ведут себя как раньше, можно хранить сумму хоть в копейках, хоть в отдельном объекте — снаружи никто ничего не заметит.
До появления # приватность была вопросом вежливости: поле называли _token, и это значило «сюда не лезь». Беда в том, что язык о таком соглашении не знает — поле остаётся обычным и ведёт себя как обычное.
| Что происходит | _token | #token |
|---|---|---|
| Чтение и запись снаружи | работают | SyntaxError ещё до запуска |
| Виден в Object.keys и JSON.stringify | да | нет |
| Поле с тем же именем в подклассе | перетирает родительское | независимо от родительского |
| Гарантия | обещание разработчика | правило языка |
Решётка работает и со статикой: static #instances = 0 — общий на класс счётчик, к которому не дотянуться извне. Комбинация «приватная статика плюс статическая фабрика» даёт классический одиночка.
Экземпляр хранится в приватном статическом поле, а наружу выставлен единственный вход — фабрика. Подменить или обнулить хранилище со стороны невозможно.
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 у готового билета возвращает пустой массив.
Счётчик и экземпляр связывает только фабрика: снаружи нет ни доступа к счётчику, ни возможности переписать номер.
Поле #number объявлено в теле класса без значения — так тоже можно, важно лишь само объявление. Геттер без сеттера делает номер доступным только на чтение.
Из подчёркивания в решётку
Есть класс термометра, где температура лежит в поле _celsius. Любой код снаружи может записать туда −500 градусов, что физически невозможно.
Перепишите класс: сделайте поле приватным, добавьте геттер celsius и метод set(value), который молча игнорирует значения ниже абсолютного нуля (−273.15). После set(25) и set(-300) геттер должен вернуть 25.
Вся защита держится на том, что записать значение можно только через метод — прямого пути к полю больше нет.
С _celsius такой гарантии не было бы: соглашение об именовании ничего не запрещает, оно лишь просит.