Проблема с подчёркиваниями
Долгое время в 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-проверок - чтобы убедиться, что объект действительно экземпляр вашего класса, прежде чем дёргать его методы.