Проблема с подчёркиваниями
Долгое время в JavaScript просто не было приватных полей. Вместо них использовали договорённость: к имени свойства добавляли подчёркивание и надеялись, что остальные разработчики не полезут туда, куда не надо:
_count выглядит приватным, но на самом деле это не так. Любой вызывающий код может его прочитать, перезаписать или удалить. Подчёркивание — это всего лишь вежливая табличка на двери; сама дверь нараспашку.
Современный JavaScript решил эту проблему — появились настоящие приватные поля, которые помечаются символом #.
Символ # делает поле приватным
Добавьте префикс # к имени поля в объявлении и в каждом месте, где вы к нему обращаетесь:
Внутри класса this.#count работает как обычное поле. А вот снаружи его просто не существует:
# — это буквально часть имени поля. Это не модификатор вроде ключевого слова private из других языков, а специальный символ, по которому парсер ищет отдельный защищённый слот у объекта. Именно поэтому ошибка вылезает ещё на этапе парсинга — до того, как код вообще запустится.
Приватные методы и геттеры
Приватными можно делать не только поля. Методы, геттеры и сеттеры тоже прекрасно работают с префиксом #:
#assertPositive — это внутренний помощник. В публичное API он не входит, и если сделать его по-настоящему приватным, никто не вызовет его извне по ошибке. А раз от него никто не зависит, вы спокойно сможете переименовать или удалить его позже.
Приватные статические поля и методы
Статические члены тоже можно делать приватными. Префикс # работает точно так же:
Приватные статические поля живут на самом классе, а не на экземплярах. Удобно для счётчиков, кэшей или конфигурации, которая не должна утекать наружу.
Наследники их не видят
Здесь часто спотыкаются те, кто пришёл из Java или C#. В JavaScript приватные поля принадлежат классу, а не экземпляру. Подкласс не может залезть в приватные поля родителя:
В JavaScript нет модификатора protected. Если подклассу нужен доступ к данным, родитель должен предоставить метод, геттер или (что встречается реже) обычное, непривaтное поле. Это сделано намеренно: приватное — значит действительно приватное, и наследование не проделывает в нём дырок.
Проверка приватного поля через in
Иногда нужно убедиться, что объект действительно принадлежит вашему классу — это так называемый brand check. Внутри класса оператор in умеет работать с именами приватных полей:
Поскольку создавать объекты с #balance умеет только Wallet, проверка #balance in obj — надёжный способ убедиться, что obj действительно экземпляр Wallet. В некоторых пограничных случаях это быстрее и безопаснее, чем instanceof: приватные поля невозможно подделать снаружи.
Типичная ловушка: у обычных объектов приватных полей нет
Приватные поля существуют только у экземпляров, созданных конструктором класса. Если попытаться обратиться к такому полю у объекта, созданного без new, получим ошибку:
Вызов метода с this, который не является экземпляром Point, выбросит ошибку прямо в рантайме. Именно так работает brand check, о котором шла речь выше: приватные поля привязаны к конкретному классу, в котором они объявлены, а не к произвольному объекту с подходящей формой.
Когда использовать #
По умолчанию делайте поля приватными всегда, когда состояние или вспомогательный метод не входит в публичный API класса. Почему это удобно:
- Свобода рефакторинга. Внешний код просто не может зацепиться за то, что ему не видно.
- Настоящая инкапсуляция в JavaScript. Никаких случайных чтений, записей или удалений снаружи.
- Чистый автокомплит. Редактор не подсовывает приватные члены тем, кто пользуется классом извне.
Публичные свойства оставляйте для того, что действительно является частью интерфейса. Если нужен доступ к приватному полю только на чтение — заведите геттер (get name()). А от соглашения с подчёркиванием (_field) пора отказаться: это был костыль на время, пока в языке не было настоящей приватности. Теперь она есть.
#celsius — это скрытое хранилище, а celsius и fahrenheit — представления только для чтения. Снаружи внутреннее состояние не испортить, а сам класс в любой момент может поменять способ хранения значения.
Дальше: прототипы
Классы в JavaScript — это по большей части синтаксический сахар над системой прототипов, более старой и фундаментальной моделью, на которой язык, собственно, и построен. Разобравшись с прототипами, вы поймёте, почему this ведёт себя именно так, как на самом деле работает наследование и что скрывается за extends у класса. Об этом — на следующей странице.
Часто задаваемые вопросы
Как объявить приватное поле в JavaScript?
Нужно поставить # перед именем поля — причём и при объявлении, и при каждом обращении. Например: class Counter { #count = 0; increment() { this.#count++; } } — это настоящее приватное поле. Важный момент: # — это часть имени, а не отдельный оператор.
В чём разница между #field и _field?
#field и _field?_field — это просто договорённость между разработчиками: свойство остаётся публичным, и любой код снаружи спокойно его читает и меняет. А #field контролирует сам язык: снаружи класса к полю не подобраться никак, попытка обратиться вызовет SyntaxError ещё на этапе парсинга. Если нужна реальная инкапсуляция — только #.
Могут ли подклассы обращаться к приватным полям родителя?
Нет. Приватные поля видны только внутри того класса, где они объявлены — даже наследники до них не дотянутся. Если подклассу нужен доступ, родительский класс должен сам предоставить метод или геттер. Это строже, чем protected в других языках, и сделано так сознательно.
Можно ли проверить, есть ли у объекта приватное поле?
Да, через оператор in внутри класса: выражение #field in obj вернёт true, если у obj есть это приватное поле. Удобно для brand-проверок — чтобы убедиться, что объект действительно экземпляр вашего класса, прежде чем дёргать его методы.