JavaScript: Особенность работы Object.freeze() с объектами имеющими приватные поля
В JavaScript для реализации иммутабельных (неизменяемых) объектов существуют несколько методов класса Object
, которые отличаются степенью накладываемых ограничений:
- Object.preventExtensions() — делает объект нерасширяемым, блокируя добавление новых свойств к объекту;
- Object.seal() — "запечатывает" объект, предотвращая любые изменения объекта, кроме изменения значений его свойств;
- Object.freeze() — самый хардкорный метод, который как-бы "замораживает" объект, предотвращая любые его изменения.
Как видно, именно Object.freeze()
накладывает самые строгие ограничения. Пример его работы можно увидеть ниже:
class Message {
text = "Hello Old World";
}
const msg = new Message();
Object.freeze(msg); // "Замораживаем" объект msg, тем самым делая его иммутабельным
msg.text = "Brave New World"
console.log(msg.text); // => "Hello Old World" - ничего не изменилось
Изначально может показаться, что Object.freeze()
гарантирует полную иммутабельность объекта, но это не совсем так. Фишка в том, с недавних пор в JavaScript помимо обычных публичных полей, стал поддерживаться специальный синтаксис приватных полей (с префиксом #
). Так вот, стоит учитывать, что Object.freeze()
не блокирует изменение значений приватных полей (с префиксом #
).
Object.freeze() во взаимодействии с различными способами реализации приватных полей
Представим, что нам стало необходимо скрыть свойство text
(сделать приватным) и контролировать к нему доступ через сеттер/геттер функции. Существует два популярных способа создания приватных свойств:
- Добавляя к имени свойства префикс
_
— что является популярным соглашением между разработчиками, обозначающим то, что свойство не должно изменяться извне. - Используя новый синтаксис приватных полей с префиксом
#
— особенность, которая встроена в сам язык.
Дальше рассмотрим примеры, как поведение объекта после замораживания через Object.freeze()
будет отличаться в зависимости от подхода.
Вначале попробуем реализовать приватное свойство с использованием префикса _
:
class Message {
_text = "Hello Old World";
setText(value) {
this._text = String(value);
}
getText() {
return this._text;
}
}
const msg = new Message();
Object.freeze(msg); // "Замораживаем" объект, чтобы сделать его иммутабельным
msg.setText("Brave New World");
console.log(msg.getText()); // => "Hello Old World" - ничего не изменилось
На примере можно заметить,что после замораживания объекта, значение его свойства msg._text
не изменяется, т.к. префикс _
является всего лишь соглашением и ничем более и поэтому это свойство, как публично доступное, становится иммутабельным.
Теперь попробуем реализовать приватные свойства через новый синтаксис с использованием префикса #
:
class Message {
#text = "Hello Old World";
setText(value) {
this.#text = String(value);
}
getText() {
return this.#text;
}
}
const msg = new Message();
Object.freeze(msg); // "Замораживаем" объект, чтобы сделать его иммутабельным
msg.setText("Brave New World");
console.log(msg.getText()); // => "Brave New World" - изменения внеслись!
В этом примере наглядно видно, что Object.freeze()
никак не влияет на работу приватных полей и соответственно после "замораживания" объекта, приватное свойство #text
остается вполне доступным для изменений.
Заключение
Я бы рекомендовал быть вообще внимательными при использовании Object.freeze()
, т.к. если вы например захотите отрефакторить старый код, переведя его на новый синтаксис приватных полей, то вы можете столкнуться с неожиданными и неприятными сюрпризами. Мало ли кто раньше при написании старого кода рассчитывал на то, что вообще все свойства будут защищены от перезаписи, а тут неожиданно выяснится, что уже и не все. Проблемы с этим могут ведь и не сразу всплыть.
Вообще, я неоднократно уже упоминал, что по многим причинам считаю реализацию новых приватных полей в JavaScript костыльной и ужасной. Благо есть TypeScript, но даже из под него часто приходится взаимодействовать с особенностями JavaScript, поэтому знание подводных камней его новых возможностей может в будущем сэкономить немало нервов, что на мой взгляд уже неплохо.