Как гарантировать, что значения моего перечисления (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.