Avatar ·

Как гарантировать, что значения моего перечисления (enum) в JavaScript не изменятся?

Всем привет! Я пишу на JavaScript и столкнулся с проблемой: я создаю объект, который использую как перечисление (enum) для статусов заказа. Например: ```javascript const OrderStatus = { PENDING: 'pending', SHIPPED: 'shipped', DELIVERED: 'delivered', CANCELLED: 'cancelled' }; ``` Но я заметил, что другие разработчики в команде или даже я сам случайно могу изменить значение этого объекта во время выполнения программы. Например, кто-то напишет `OrderStatus.PENDING = 'new_pending'` и сломает всю логику. В TypeScript есть нормальные enum, но мы пишем на чистом JS. Я пробовал использовать `Object.freeze()`, но не уверен, что это 100% защита от изменений. Может быть, есть более надёжный способ сделать константы перечисления неизменяемыми? Как правильно организовать enum в JavaScript, чтобы их нельзя было изменить ни случайно, ни намеренно? Нужна полноценная защита на уровне кода, чтобы если кто-то попытается изменить значение, это либо выбросило ошибку, либо просто игнорировалось. Подскажите, пожалуйста, лучшие практики! <<>>

Проблема мутабельности объектов в JavaScript

Вы абсолютно правы: обычные объекты в JavaScript, даже объявленные через const, не защищают свои свойства от изменений. Ключевое слово const защищает только саму ссылку на объект, а не его содержимое. Поэтому OrderStatus.PENDING = 'new_pending' выполнится без ошибок. Давайте разберём надёжные способы создания неизменяемых перечислений.

Решение 1: Object.freeze() — базовый уровень защиты

Object.freeze() делает объект полностью неизменяемым: нельзя добавлять, удалять или изменять существующие свойства. Это работает на поверхностном уровне (shallow freeze).

const OrderStatus = Object.freeze({
  PENDING: 'pending',
  SHIPPED: 'shipped',
  DELIVERED: 'delivered',
  CANCELLED: 'cancelled'
});

// Попытка изменить значение
OrderStatus.PENDING = 'new_pending'; // В строгом режиме ('use strict') — TypeError
// В нестрогом режиме — просто игнорируется
console.log(OrderStatus.PENDING); // 'pending' — значение не изменилось

// Попытка добавить новое свойство
OrderStatus.REFUNDED = 'refunded'; // Игнорируется или TypeError
console.log(OrderStatus.REFUNDED); // undefined

// Попытка удалить свойство
delete OrderStatus.SHIPPED; // Игнорируется или TypeError
console.log(OrderStatus.SHIPPED); // 'shipped'

Важно: В строгом режиме ("use strict") попытка изменить замороженный объект вызовет TypeError, что поможет быстро обнаружить ошибку. Рекомендую всегда использовать строгий режим в проектах.

Решение 2: Глубокое замораживание (deep freeze) для сложных структур

Если ваше перечисление содержит вложенные объекты (например, метаданные для каждого статуса), Object.freeze() не защитит вложенные уровни. Решение — рекурсивная функция:

function deepFreeze(obj) {
  Object.keys(obj).forEach(key => {
    const value = obj[key];
    if (value && typeof value === 'object' && !Object.isFrozen(value)) {
      deepFreeze(value);
    }
  });
  return Object.freeze(obj);
}

const OrderStatus = deepFreeze({
  PENDING: { label: 'Ожидание', color: 'yellow' },
  SHIPPED: { label: 'Отправлен', color: 'blue' },
  DELIVERED: { label: 'Доставлен', color: 'green' }
});

OrderStatus.PENDING.color = 'red'; // TypeError в строгом режиме

Решение 3: Геттеры и замыкания — максимальная защита

Можно создать перечисление через замыкание, где значения доступны только через геттеры, а запись запрещена на уровне функции:

function createEnum(values) {
  const enumObj = {};
  Object.keys(values).forEach(key => {
    Object.defineProperty(enumObj, key, {
      value: values[key],
      writable: false,
      configurable: false,
      enumerable: true
    });
  });
  return Object.freeze(enumObj);
}

const OrderStatus = createEnum({
  PENDING: 'pending',
  SHIPPED: 'shipped',
  DELIVERED: 'delivered'
});

// Любая попытка изменить свойство вызовет TypeError
OrderStatus.PENDING = 'new'; // TypeError в строгом режиме
Object.defineProperty(OrderStatus, 'PENDING', { value: 'new' }); // TypeError

Решение 4: Proxy — с кастомной логикой

Используйте Proxy для полного контроля над доступом к свойствам:

const OrderStatus = new Proxy({
  PENDING: 'pending',
  SHIPPED: 'shipped',
  DELIVERED: 'delivered'
}, {
  set(target, property, value) {
    throw new Error(`Невозможно изменить свойство "${property}" enum`);
  },
  deleteProperty(target, property) {
    throw new Error(`Невозможно удалить свойство "${property}" enum`);
  }
});

OrderStatus.PENDING = 'new'; // Error: Невозможно изменить свойство "PENDING" enum
delete OrderStatus.SHIPPED;   // Error: Невозможно удалить свойство "SHIPPED" enum

Рекомендация

  • Для большинства проектов достаточно Object.freeze() в строгом режиме.
  • Если нужна максимальная защита от случайных изменений — используйте Proxy или геттеры.
  • Добавьте в проект линтер (ESLint с правилом no-const-assign), чтобы статически проверять подобные ошибки.
  • Рассмотрите возможность использования TypeScript — его enum компилируются в защищённый код.

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

<<>>

Альтернативный подход: Символы (Symbols) и Map

Если вы хотите гарантировать, что значения перечисления нельзя подделать или случайно использовать строку вместо константы, используйте Symbol в качестве значений:

const OrderStatus = Object.freeze({
  PENDING: Symbol('pending'),
  SHIPPED: Symbol('shipped'),
  DELIVERED: Symbol('delivered')
});

// Теперь нельзя случайно написать if (status === 'pending')
// Только строгое сравнение с символом:
if (status === OrderStatus.PENDING) {
  console.log('Заказ ожидает обработки');
}

Плюсы:

  • Символы уникальны — нельзя создать "поддельное" значение из строки.
  • Хорошо сочетается с Object.freeze().

Минусы:

  • Сложнее отлаживать, так как Symbol не имеет понятного строкового представления.
  • Нельзя сериализовать в JSON (символы теряются).

Использование Map для динамических enum

Если перечисление должно быть динамическим (добавлять значения в рантайме), используйте Map с заморозкой:

const OrderStatus = new Map([
  ['PENDING', 'pending'],
  ['SHIPPED', 'shipped'],
  ['DELIVERED', 'delivered']
]);

// Защита от изменений через заморозку Map
Object.freeze(OrderStatus);
// Теперь нельзя добавить/удалить записи, но можно изменить значения
OrderStatus.set('PENDING', 'new'); // Ошибка или игнорирование

// Для полной защиты — заморозьте и значения
OrderStatus.forEach((value, key) => {
  OrderStatus.set(key, Object.freeze(value));
});

Выбирайте подход под свою задачу: для простых enum — Object.freeze, для уникальности — Symbol, для динамики — Map.