Разбор внутренних атрибутов свойств, различий между дескрипторами данных и доступа, а также методов Object.defineProperty, seal и freeze.

Дескрипторы свойств объектов в JavaScript

Дескриптор свойства — это внутренний «паспорт» любого поля объекта, определяющий не только его значение, но и строгие правила поведения: можно ли его изменить, удалить или увидеть при переборе.

Когда мы пишем obj.name = "Ivan", JavaScript не просто записывает значение. Под капотом он создаёт свойство и автоматически назначает ему набор внутренних атрибутов (флагов). В большинстве случаев эти настройки нас устраивают, но для создания надёжных библиотек, защиты данных или реализации реактивности требуется тонкий контроль. Именно здесь на сцену выходят дескрипторы.

Понимание дескрипторов критически важно для осознанного управления состоянием объектов. Они позволяют эмулировать приватные свойства, создавать константы и перехватывать операции чтения и записи. Методы Object.seal() и Object.freeze(), ограничивающие изменение объектов, также работают исключительно через модификацию этих дескрипторов.

Ниже последовательно разбираются: структура дескрипторов, разница между дескрипторами данных и доступа, а также практические сценарии их применения.

Что такое дескриптор и как его увидеть

Дескриптор — это обычный объект JavaScript, описывающий конфигурацию конкретного свойства. Получить его можно с помощью метода Object.getOwnPropertyDescriptor(obj, propName).

const user = { name: "Ivan" };

const descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// {
//   value: "Ivan",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Существует два взаимно исключающих типа дескрипторов: дескрипторы данных (хранят значение) и дескрипторы доступа (используют геттеры и сеттеры). Они не могут присутствовать в одном дескрипторе одновременно.

Дескрипторы данных (Data Descriptors)

Описывают обычное свойство со значением. Управляется четырьмя флагами:

  • value — само значение свойства.
  • writable (true/false) — можно ли перезаписать значение.
  • enumerable (true/false) — видно ли свойство в циклах (for..in, Object.keys) и при сериализации (JSON.stringify).
  • configurable (true/false) — можно ли удалить свойство через delete и изменить его дескриптор в будущем.
const config = { apiUrl: "https://api.example.com" };

Object.defineProperty(config, "apiUrl", {
  writable: false,      // Запрет на изменение значения
  configurable: false   // Запрет на удаление и повторное изменение дескриптора
});

config.apiUrl = "https://evil.com"; // Игнорируется (или ошибка в strict mode)
delete config.apiUrl;               // Игнорируется (или ошибка в strict mode)

Дескрипторы доступа (Accessor Descriptors)

Используются, когда значение свойства должно вычисляться динамически или требовать валидации при записи. Вместо value и writable они используют функции:

  • get() — функция, вызываемая при чтении свойства.
  • set(value) — функция, вызываемая при попытке записи.
const user = {
  firstName: "Ivan",
  lastName: "Petrov"
};

Object.defineProperty(user, "fullName", {
  enumerable: true,
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  set(value) {
    const parts = value.split(" ");
    this.firstName = parts[0];
    this.lastName = parts[1] || "";
  }
});

console.log(user.fullName); // "Ivan Petrov" (сработал get)
user.fullName = "Sidorov Alex"; // сработал set
console.log(user.firstName); // "Sidorov"

Массовое управление: seal и freeze

Если нужно изменить дескрипторы всех свойств объекта сразу, используются специальные методы. Они работают только с дескрипторами данных и не затрагивают вложенные объекты (поверхностная защита).

  • Object.seal(obj) — делает все свойства configurable: false. Нельзя добавлять или удалять свойства, нельзя менять дескрипторы, но можно менять значения (если они были writable).
  • Object.freeze(obj) — делает все свойства configurable: false и writable: false. Объект становится полностью неизменяемым (иммутабельным).
const data = { status: "active", role: "admin" };
Object.freeze(data);

data.role = "user";       // ❌ Не сработает
data.newProp = 1;         // ❌ Не сработает
delete data.status;       // ❌ Не сработает

console.log(Object.isFrozen(data)); // true

Практическое применение

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

  1. Создание констант и защита конфигурации: предотвращение случайного переопределения критически важных настроек приложения.
  2. Инкапсуляция: скрытие внутренних служебных свойств (например, _internalCache) от циклов перебора и JSON.stringify путём установки enumerable: false.
  3. Валидация данных: использование сеттеров для проверки значений перед их записью в объект.
  4. Реактивность: фреймворк Vue.js (версии 2.x) делал данные реактивными именно через Object.defineProperty, подменяя геттеры и сеттеры для отслеживания изменений и перерисовки интерфейса.

Важный нюанс: значения по умолчанию

При создании свойства через обычное присваивание (obj.a = 1) все флаги по умолчанию равны true. Однако при использовании Object.defineProperty любые не указанные явно флаги по умолчанию считаются false.

const obj = {};
Object.defineProperty(obj, "test", { value: 42 });

// По умолчанию: writable: false, enumerable: false, configurable: false
obj.test = 100; // Ошибка в strict mode
console.log(Object.keys(obj)); // [] (свойство скрыто)

Что запомнить

  • Дескриптор управляет поведением свойства: writable (запись), enumerable (перебор), configurable (удаление/изменение настроек).
  • Существует два типа дескрипторов: данных (value) и доступа (get/set). Они не смешиваются.
  • Object.defineProperty устанавливает флаги в false по умолчанию, если они не указаны явно.
  • Object.seal() запрещает добавление/удаление свойств, но разрешает изменение значений.
  • Object.freeze() запрещает любые изменения объекта (поверхностно).
  • Для глубокой заморозки вложенных объектов требуется рекурсивный обход.
  • В строгом режиме ("use strict") нарушения ограничений дескрипторов вызывают TypeError, а не молча игнорируются.