Пошук уроків, статей та іншого контенту
Розглянете Object.freeze, Object.seal і Object.preventExtensions для обмеження змін об’єктів.
Об’єкти в JavaScript змінювані за замовчуванням:
const config = {
host: "localhost",
port: 3000
};
config.port = 8080;
config.debug = true;
delete config.host;Це зручно, але іноді небезпечно. Наприклад:
конфігурація може бути випадково змінена після ініціалізації;
об’єкт стану може змінитися з неочікуваного місця;
API може вимагати стабільної структури об’єкта;
сторонній код може додати або видалити властивості;
помилки мутації можуть залишатися непомітними.
JavaScript надає три основні методи для обмеження змін:
Object.preventExtensions();
Object.seal();
Object.freeze().
Усі вони змінюють сам об’єкт і повертають посилання на нього.
Object.preventExtensions()Метод Object.preventExtensions(object) забороняє додавати нові власні властивості до об’єкта.
Водночас уже наявні властивості можна:
змінювати;
видаляти;
переналаштовувати, якщо це дозволено їхніми дескрипторами.
"use strict";
const user = {
name: "Olena",
age: 28
};
Object.preventExtensions(user);
user.age = 29;
delete user.name;
try {
user.email = "olena@example.com";
} catch (error) {
console.log(error.name); // TypeError
}
console.log(user); // { age: 29 }
console.log(Object.isExtensible(user)); // falseУ нестрогому режимі спроба додати властивість до нерасширюваного об’єкта зазвичай ігнорується без помилки:
const settings = {
theme: "dark"
};
Object.preventExtensions(settings);
settings.language = "uk";
console.log(settings.language); // undefinedДля коду, де важливо виявляти помилки мутації, доцільно використовувати строгий режим.
Метод Object.isExtensible() повертає true, якщо до об’єкта можна додавати властивості:
const object = {};
console.log(Object.isExtensible(object)); // true
Object.preventExtensions(object);
console.log(Object.isExtensible(object)); // falseObject.seal()Метод Object.seal(object):
забороняє додавати нові властивості;
забороняє видаляти наявні властивості;
робить усі наявні властивості неконфігурованими.
Значення властивостей, які мають writable: true, усе ще можна змінювати.
"use strict";
const account = {
owner: "Andrii",
balance: 1000
};
Object.seal(account);
account.balance = 1500;
try {
account.currency = "UAH";
} catch (error) {
console.log(error.name); // TypeError
}
try {
delete account.owner;
} catch (error) {
console.log(error.name); // TypeError
}
console.log(account);
// { owner: "Andrii", balance: 1500 }
console.log(Object.isSealed(account)); // trueПісля seal() не можна змінити атрибут configurable на true. Також не можна видалити властивість.
Проте writable можна змінити з true на false, якщо це дозволено правилами дескрипторів:
"use strict";
const product = {
title: "Keyboard"
};
Object.seal(product);
Object.defineProperty(product, "title", {
writable: false
});
try {
product.title = "Mouse";
} catch (error) {
console.log(error.name); // TypeError
}
console.log(product.title); // KeyboardObject.freeze()Метод Object.freeze(object) є найсуворішим із трьох.
Він:
забороняє додавати нові властивості;
забороняє видаляти наявні властивості;
робить властивості неконфігурованими;
робить звичайні властивості незаписуваними.
"use strict";
const config = {
host: "localhost",
port: 3000
};
Object.freeze(config);
try {
config.port = 8080;
} catch (error) {
console.log(error.name); // TypeError
}
try {
config.debug = true;
} catch (error) {
console.log(error.name); // TypeError
}
try {
delete config.host;
} catch (error) {
console.log(error.name); // TypeError
}
console.log(config);
// { host: "localhost", port: 3000 }
console.log(Object.isFrozen(config)); // trueПеревірити стан об’єкта можна за допомогою Object.isFrozen():
const data = {
id: 10
};
Object.freeze(data);
console.log(Object.isFrozen(data)); // true| Метод | Додавання | Видалення | Зміна значення | Зміна дескрипторів | |---|---|---|---|---| | preventExtensions() | Ні | Так | Так | Частково | | seal() | Ні | Ні | Так, якщо writable: true | Обмежено | | freeze() | Ні | Ні | Ні для звичайних властивостей | Ні |
Object.freeze() фактично включає обмеження Object.seal(), а Object.seal() — обмеження Object.preventExtensions().
Усі три методи працюють лише з самим об’єктом. Вони не заморожують вкладені об’єкти автоматично.
"use strict";
const settings = {
theme: "dark",
editor: {
tabSize: 2
}
};
Object.freeze(settings);
try {
settings.theme = "light";
} catch (error) {
console.log(error.name); // TypeError
}
settings.editor.tabSize = 4;
console.log(settings.editor.tabSize); // 4Властивість settings.editor після заморожування не можна переприсвоїти, але сам вкладений об’єкт залишається змінюваним.
"use strict";
const state = Object.freeze({
user: {
name: "Ihor"
}
});
try {
state.user = { name: "Marta" };
} catch (error) {
console.log(error.name); // TypeError
}
state.user.name = "Marta";
console.log(state.user.name); // MartaЯкщо потрібно захистити всю структуру, можна рекурсивно заморозити вкладені об’єкти.
Важливо врахувати циклічні посилання. Для цього використовується WeakSet:
"use strict";
function deepFreeze(value, visited = new WeakSet()) {
if (value === null || typeof value !== "object") {
return value;
}
if (visited.has(value)) {
return value;
}
visited.add(value);
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
// Обробляємо значення властивості, але не викликаємо getter.
if (descriptor && "value" in descriptor) {
deepFreeze(descriptor.value, visited);
}
}
return Object.freeze(value);
}
const application = {
name: "Editor",
options: {
theme: "dark",
shortcuts: {
save: "Ctrl+S"
}
}
};
application.self = application;
deepFreeze(application);
try {
application.options.theme = "light";
} catch (error) {
console.log(error.name); // TypeError
}
try {
application.options.shortcuts.save = "Cmd+S";
} catch (error) {
console.log(error.name); // TypeError
}
console.log(application.options.theme); // dark
console.log(application.options.shortcuts.save); // Ctrl+SReflect.ownKeys()Object.keys() повертає лише enumerable-властивості зі строковими ключами. Reflect.ownKeys() охоплює:
enumerable і non-enumerable властивості;
строкові ключі;
Symbol-ключі.
Тому Reflect.ownKeys() краще підходить для повного обходу структури.
Getter може виконувати код або мати побічні ефекти. Наприклад:
const object = {
get value() {
console.log("Getter викликано");
return {
nested: true
};
}
};Безпечніше аналізувати дескриптор і рекурсивно обробляти лише descriptor.value. Getter і setter самі по собі не містять доступного для рекурсивного обходу значення.
Захист об’єктів базується на дескрипторах властивостей.
Для звичайної властивості дескриптор має атрибути:
value — значення;
writable — чи можна змінювати значення;
enumerable — чи потрапляє властивість у перелічення;
configurable — чи можна змінювати дескриптор і видаляти властивість.
const object = {
name: "Nazar"
};
console.log(Object.getOwnPropertyDescriptor(object, "name"));
// {
// value: "Nazar",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.freeze(object);
console.log(Object.getOwnPropertyDescriptor(object, "name"));
// {
// value: "Nazar",
// writable: false,
// enumerable: true,
// configurable: false
// }Object.freeze() не змінює enumerable, але змінює writable і configurable.
Заморожування accessor-властивості не забороняє виконання її setter.
"use strict";
let internalValue = 0;
const object = {
get value() {
return internalValue;
},
set value(nextValue) {
internalValue = nextValue;
}
};
Object.freeze(object);
object.value = 42;
console.log(object.value); // 42Саме властивість value залишається незмінною як властивість об’єкта, але setter може змінити зовнішню змінну internalValue.
Тому Object.freeze() не є універсальним бар’єром від будь-яких побічних ефектів. Він контролює структуру об’єкта та його дескриптори, а не поведінку функцій, getter-ів і setter-ів.
Захист об’єкта не заморожує його прототип:
"use strict";
const prototype = {
role: "user"
};
const user = Object.create(prototype);
Object.freeze(user);
prototype.role = "admin";
console.log(user.role); // adminУ цьому прикладі власних властивостей у user немає. Значення береться з прототипу, який залишився змінюваним.
Якщо потрібно захистити ієрархію прототипів, кожен об’єкт потрібно обробляти окремо.
Map, Set та інші об’єкти з внутрішнім станомObject.freeze() не заморожує внутрішні структури Map і Set:
"use strict";
const permissions = new Set(["read"]);
Object.freeze(permissions);
permissions.add("write");
console.log(permissions.has("write")); // trueУ цьому випадку метод add() змінює внутрішній стан Set, а не звичайну власну властивість об’єкта.
Для незмінної структури можна:
не передавати Map і Set у код, який може їх змінити;
створювати копії перед передаванням;
використовувати власну абстракцію з контрольованими операціями;
зберігати дані у звичайних об’єктах або масивах і глибоко їх заморожувати, якщо це відповідає задачі.
Подібні міркування стосуються й інших об’єктів із внутрішнім станом. Object.freeze() не перетворює будь-який об’єкт на повністю незмінну математичну структуру.
Масиви також є об’єктами, тому їх можна заморожувати або запечатувати.
"use strict";
const numbers = [1, 2, 3];
Object.freeze(numbers);
try {
numbers.push(4);
} catch (error) {
console.log(error.name); // TypeError
}
try {
numbers[0] = 100;
} catch (error) {
console.log(error.name); // TypeError
}
console.log(numbers); // [1, 2, 3]Заморожування масиву забороняє:
змінювати елементи;
змінювати length;
додавати елементи;
видаляти елементи.
Але вкладені елементи-об’єкти все ще потребують рекурсивного заморожування:
"use strict";
const list = Object.freeze([
{ id: 1 },
{ id: 2 }
]);
list[0].id = 100;
console.log(list[0].id); // 100Object.defineProperty()Методи захисту обмежують не лише звичайний синтаксис присвоєння, а й операції через Object.defineProperty():
"use strict";
const object = {
value: 10
};
Object.freeze(object);
try {
Object.defineProperty(object, "value", {
value: 20
});
} catch (error) {
console.log(error.name); // TypeError
}
try {
Object.defineProperty(object, "extra", {
value: true
});
} catch (error) {
console.log(error.name); // TypeError
}Заморожений об’єкт не дозволяє змінювати дескриптори власних властивостей або додавати нові.
Object.preventExtensions()Підходить, коли:
структура об’єкта вже сформована;
наявні значення ще повинні змінюватися;
видалення властивостей допустиме;
потрібно заборонити лише появу нових властивостей.
Object.seal()Підходить, коли:
набір властивостей має залишатися стабільним;
властивості не можна видаляти;
значення властивостей повинні оновлюватися;
потрібно заборонити зміну конфігурації властивостей.
Object.freeze()Підходить, коли:
об’єкт є незмінною конфігурацією;
дані потрібно безпечно передати іншому коду;
зміни мають створювати нову версію об’єкта;
потрібно виявляти випадкові мутації.
Наприклад, замість зміни замороженого об’єкта можна створити новий:
"use strict";
const originalConfig = Object.freeze({
theme: "dark",
language: "uk"
});
const updatedConfig = {
...originalConfig,
theme: "light"
};
console.log(originalConfig.theme); // dark
console.log(updatedConfig.theme); // lightconst object = Object.freeze({
nested: {
enabled: true
}
});
object.nested.enabled = false;nested не був заморожений. Для повного захисту потрібне рекурсивне заморожування.
seal() і freeze()Object.seal() не забороняє змінювати значення writable-властивостей:
const object = {
count: 1
};
Object.seal(object);
object.count = 2;
console.log(object.count); // 2Для заборони зміни значень потрібно використовувати Object.freeze().
У нестрогому режимі деякі помилкові операції над захищеними об’єктами не повідомляють про проблему, а просто ігноруються.
Для виявлення таких помилок використовуйте:
"use strict";або модулі JavaScript, які виконуються у строгому режимі.
Метод не створює копію:
const object = {
value: 1
};
const result = Object.freeze(object);
console.log(result === object); // trueВін повертає те саме посилання, лише змінюючи правила роботи з об’єктом.
Якщо інший код уже отримав посилання на вкладений об’єкт, поверхневе заморожування не захистить цей вкладений об’єкт:
const nested = {
enabled: true
};
const settings = Object.freeze({ nested });
nested.enabled = false;
console.log(settings.nested.enabled); // falseПотрібно заморожувати структуру до її передачі або використовувати глибоке заморожування.
Map або Set через freeze()Object.freeze() не забороняє map.set() або set.add(). Для таких структур потрібен інший підхід до контролю мутацій.
Object.preventExtensions() забороняє додавати нові властивості.
Object.seal() додатково забороняє видаляти властивості та змінювати їхню конфігурацію.
Object.freeze() також забороняє змінювати значення звичайних властивостей.
Усі три методи працюють поверхнево.
Object.isExtensible(), Object.isSealed() і Object.isFrozen() допомагають перевірити стан об’єкта.
У строгому режимі спроби заборонених змін зазвичай призводять до TypeError.
Getter-и, setter-и, прототипи, Map, Set та інші внутрішні структури потребують окремого аналізу.
Для складних вкладених даних можна реалізувати deepFreeze() з використанням WeakSet і Reflect.ownKeys().
Захист об’єкта не замінює продуманий дизайн даних: часто краще створювати нові об’єкти замість мутації наявних.