Авторский проект IT-специалиста Олега Барабанова Персональные публикации на тему IT и не только…

JavaScript: Особенность работы Object.freeze() с объектами имеющими приватные поля

В JavaScript для реализации иммутабельных (неизменяемых) объектов существуют несколько методов класса Object, которые отличаются степенью накладываемых ограничений:

Как видно, именно 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 (сделать приватным) и контролировать к нему доступ через сеттер/геттер функции. Существует два популярных способа создания приватных свойств:

  1. Добавляя к имени свойства префикс _ — что является популярным соглашением между разработчиками, обозначающим то, что свойство не должно изменяться извне.
  2. Используя новый синтаксис приватных полей с префиксом # — особенность, которая встроена в сам язык.

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