Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Object.freeze(), object.seal() та object.assign() у JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Object.freeze()** повністю блокує об'єкт: заборонені додавання, видалення та зміна значень. **Object.seal()** фіксує структуру, але дозволяє оновлювати існуючі значення. **Object.assign()** копіює перераховувані власні властивості із джерел до цільового об'єкта, поверхнево. ```javascript Object.freeze({ a: 1 }).a = 2; // Не спрацює Object.seal({ a: 1 }).a = 2; // Спрацює Object.assign({}, { a: 1 }, { b: 2 }); // { a: 1, b: 2 } ``` **Головне:** всі три поверхневі - вкладені об'єкти залишаються мутабельними.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Object.freeze(), Object.seal() та Object.assign()** - це три статичні методи на `Object`, які вирішують дві різні задачі: freeze і seal контролюють незмінність об'єктів, а assign копіює властивості між ними. ## Теорія ### TL;DR - `Object.freeze()` блокує все: не можна ні додавати, ні видаляти, ні змінювати значення. Замкнений сейф. - `Object.seal()` фіксує структуру, але дозволяє оновлювати значення. Запаяна коробка. - `Object.assign()` копіює перераховувані власні властивості із джерел до цільового об'єкта, поверхнево. Ксерокс. - Всі три поверхневі: вкладені об'єкти залишаються мутабельними незалежно від того, що застосовано до батьківського. - Правило вибору: незмінний конфіг або константи - freeze. Фіксована форма, але значення оновлюються - seal. Клонування або злиття - assign. ### Швидкий приклад ```javascript const base = { a: 1, b: { c: 2 } }; const frozen = Object.freeze({ ...base }); const sealed = Object.seal({ ...base }); const copied = Object.assign({}, base); frozen.a = 99; // Не спрацює (TypeError у strict mode) frozen.b.c = 99; // Спрацює! Вкладений об'єкт не заморожений sealed.a = 99; // Спрацює - існуючі значення можна змінювати sealed.d = 4; // Не спрацює - нові властивості заборонені copied.a = 99; // Спрацює - копія незалежна на верхньому рівні console.log(base.a); // 1 - оригінал не змінився ``` Три методи, три різні поведінки. Зверни увагу: `frozen.b.c` все одно мутується, бо freeze блокує лише верхній рівень. Це класична пастка. ### Ключова різниця `Object.freeze()` встановлює кожен дескриптор властивості у `{ writable: false, configurable: false }` і позначає об'єкт нерозширюваним. `Object.seal()` також позначає об'єкт нерозширюваним і встановлює `configurable: false`, але залишає `writable: true` - тому існуючі значення можна змінювати, а структуру - ні. `Object.assign()` не має нічого спільного з незмінністю: він ітерує джерела і копіює їх перераховувані власні властивості до цільового об'єкта поверхнево, читаючи через `[[Get]]` і записуючи через `[[Set]]`. ### Коли що використовувати - Константи на рівні всього застосунку, типи дій Redux або конфіг середовища: `Object.freeze()` - Схема форми або форма (shape) API-відповіді, де структура фіксована, але значення полів оновлюються: `Object.seal()` - Злиття налаштувань користувача з дефолтами без мутації жодного з них: `Object.assign({}, defaults, userOptions)` - Поверхневий клон перед трансформацією: `Object.assign({}, obj)` або spread-еквівалент `{ ...obj }` - Заповнити відсутні поля на існуючому об'єкті з кількох джерел: `Object.assign(target, source1, source2)` ### Таблиця порівняння | Функціональність | Object.freeze() | Object.seal() | Object.assign() | |---|---|---|---| | Блокує додавання | Так | Так | Ні (копіює до target) | | Блокує видалення | Так | Так | Ні | | Блокує зміну значень | Так | Ні | Ні | | Копіює властивості | Ні | Ні | Так (поверхнево) | | Вкладені об'єкти | Тільки поверхнево | Тільки поверхнево | Тільки поверхнево | | Типовий сценарій | Redux-константи, конфіг | Схеми форм, напів-фіксований стан | Клонування/злиття, defaultProps | `Object.preventExtensions()` знаходиться нижче за обидва: він лише блокує додавання нових властивостей, залишаючи видалення та зміну значень відкритими. Freeze - найсуворіший, далі seal, далі preventExtensions. ### Як це працює в рушії V8 реалізує `Object.freeze()` через виклик `PreventExtensions` на об'єкті, а потім ітерує всі власні дескриптори властивостей і встановлює кожен у `{ writable: false, configurable: false }`. Операції доступу до властивостей перевіряють ці прапори під час Set та Delete в intrinsics рушія. `Object.seal()` також викликає `PreventExtensions` і встановлює `configurable: false` для кожного дескриптора, але пропускає прапор `writable`. Саме ця одна різниця пояснює, чому `sealed.a = 99` спрацьовує, а `frozen.a = 99` - ні. `Object.assign()` запускає C++-цикл по кожному джерелу: перевіряє `HasOwnProperty`, читає через `GetOwnPropertyDescriptor`, потім записує до цільового об'єкта через `DefineOwnProperty`. Неперераховувані властивості та властивості ланцюжка прототипів пропускаються повністю. Оскільки читання відбувається через `[[Get]]`, а не через копіювання дескриптора, геттери викликаються і їхнє повернуте значення зберігається на target як звичайна data-властивість. Це важливо знати з практичного досвіду: якщо мутуєш заморожену властивість у strict mode, V8 кидає `TypeError` одразу. У loose mode рушій просто повертає об'єкт без змін і без жодної помилки. Це мовчазне ігнорування - реальне джерело багів у продакшені, де помилка виникає далеко від місця мутації. ### Типові помилки **1. Думати, що freeze глибокий** ```javascript const settings = Object.freeze({ db: { host: 'localhost' } }); settings.db.host = 'prod-server'; // Спрацює - мутація пройшла console.log(settings.db.host); // 'prod-server' ``` Посилання на `db` не можна замінити, але сам об'єкт, на який воно вказує, - звичайний мутабельний. Freeze заблокував лише обгортку. Рішення: рекурсивний deep freeze. ```javascript function deepFreeze(obj) { Object.freeze(obj); Object.getOwnPropertyNames(obj).forEach(key => { const val = obj[key]; if (val && typeof val === 'object') deepFreeze(val); }); return obj; } ``` **2. Використовувати assign для глибокого клонування** ```javascript const orig = { nest: { val: 1 } }; const copy1 = Object.assign({}, orig); const copy2 = Object.assign({}, orig); copy1.nest.val = 99; console.log(copy2.nest.val); // 99 - спільне посилання! ``` Обидві копії посилаються на один і той самий об'єкт `nest`, бо assign скопіював лише посилання. Рішення: `structuredClone(obj)` (Node 17+, сучасні браузери) або `JSON.parse(JSON.stringify(obj))` для простих серіалізованих даних. **3. Очікувати, що assign копіює дескриптори властивостей** ```javascript const source = Object.create(null, { id: { value: 42, writable: false, enumerable: true } }); const target = Object.assign({}, source); target.id = 100; // Спрацює! writable:false не перенісся console.log(target.id); // 100 ``` `Object.assign()` читає значення і записує його як звичайну data-властивість. Прапор `writable: false` із дескриптора джерела зникає. Для копіювання повних дескрипторів використовуй `Object.getOwnPropertyDescriptors()` разом із `Object.defineProperties()`. **4. Пропуск неперераховуваних властивостей** ```javascript const obj = { pub: 1 }; Object.defineProperty(obj, 'secret', { value: 2, enumerable: false }); const copy = Object.assign({}, obj); console.log(copy.secret); // undefined - пропущено ``` Assign торкається лише перераховуваних власних властивостей. Символьні ключі з `enumerable: false` також пропускаються. Це ловить людей, які додають метадані через `defineProperty` і потім дивуються, чому вони зникають після злиття. **5. Мовчазна помилка поза strict mode** ```javascript const frozen = Object.freeze({ name: 'Alice' }); frozen.name = 'Bob'; // Ніякої помилки у loose mode console.log(frozen.name); // 'Alice' - зміна проігнорована ``` Завжди запускай код у strict mode, щоб `TypeError` виникав одразу. Якщо треба перевірити стан програмно - `Object.isFrozen(obj)`, `Object.isSealed(obj)` та `Object.isExtensible(obj)` дають точну відповідь. ### Де зустрічається в реальних проектах - React (застарілі класові компоненти): `Object.assign()` використовувався всередині для злиття (merge) `defaultProps` із пропсами, що приходять - Redux Toolkit: `Object.freeze()` на константах типів дій, щоб заблокувати випадкову мутацію в редьюсерах - Express та body-parser: `Object.assign()` для злиття дефолтних опцій middleware з налаштуваннями користувача - CLI-інструменти на Node.js (webpack config): `Object.seal()` на підмножинах `process.env` для фіксації структури - Lodash: `_.assign()` як кросбраузерний поліфіл; окрема утиліта `deepFreeze` у модулі utils ### Питання на співбесіді **Q:** Яка різниця в дескрипторах властивостей між freeze і seal? **A:** `Object.freeze()` встановлює `writable: false` і `configurable: false` для кожної власної властивості. `Object.seal()` встановлює лише `configurable: false` і позначає об'єкт нерозширюваним, але залишає `writable: true`. Саме цей один прапор визначає, чому запечатаний об'єкт приймає зміну значень, а заморожений - ні. **Q:** Чи працює `Object.assign()` з об'єктами без прототипу (null-prototype)? **A:** Так. Він копіює перераховувані власні властивості поверхнево незалежно від ланцюжка прототипів джерела. Прототип ніколи не читається і не змінюється. **Q:** Як перевірити стан об'єкта - чи він заморожений, запечатаний або розширюваний? **A:** `Object.isFrozen(obj)`, `Object.isSealed(obj)` та `Object.isExtensible(obj)`. Заморожений об'єкт вважається також запечатаним і нерозширюваним, але зворотне не вірно. `isFrozen` повертає `true` тільки коли всі власні властивості також `non-writable` і `non-configurable`. **Q:** Який вплив заморожування на продуктивність? **A:** Саме заморожування - одноразова операція з мінімальними витратами. Проблема виникає при виклику freeze або seal всередині гарячих циклів. Для примітивних значень вистачає `const` - freeze потрібен тільки для об'єктів. **Q:** (Senior) Чому `Object.assign()` викликає геттери з джерела замість копіювання accessor-дескриптора? **A:** За специфікацією `Object.assign()` використовує `[[Get]]` на джерелі і `[[Set]]` на цільовому об'єкті, а не `[[GetOwnPropertyDescriptor]]` і `[[DefineOwnProperty]]`. Геттер викликається, а його результат записується як звичайна data-властивість. Щоб скопіювати повний accessor-дескриптор, використовуй `Object.getOwnPropertyDescriptors()` разом із `Object.create()` або `Object.defineProperties()`. **Q:** (Senior) Чому у V8 мутація замороженої властивості кидає помилку в strict mode, але не в loose mode? **A:** Операція Set перевіряє прапор `writable` у дескрипторі. У strict mode провалений запис іде по Throw-шляху і кидає `TypeError`. У sloppy mode semantics та сама перевірка провалюється, але Throw-шлях пропускається - рушій повертає без мутації і без помилки. Це поведінка, визначена специфікацією, а не особливість V8. ## Приклади ### Злиття конфігурації користувача з дефолтами Типовий патерн в Express middleware, React-конфігурації та CLI-інструментах. ```javascript const defaults = { theme: 'light', apiKey: 'default-key', debug: false }; const userConfig = { theme: 'dark' }; // Часткове перевизначення const config = Object.assign({}, defaults, userConfig); // { theme: 'dark', apiKey: 'default-key', debug: false } // Заморожуємо, щоб жоден модуль не мутував конфіг const frozenConfig = Object.freeze(config); frozenConfig.theme = 'light'; // Не спрацює - конфіг захищений ``` Пізніші джерела перезаписують ранніші, тому `userConfig.theme` виграє над `defaults.theme`. Пустий `{}` першим аргументом гарантує, що `defaults` сам не мутується - `Object.assign()` мутує target, і тут target - це свіжий порожній об'єкт. ### Пастка зі спільним вкладеним посиланням Найпоширеніший реальний баг від використання assign для клонування. ```javascript const template = { name: 'untitled', meta: { version: 1, tags: [] } }; const doc1 = Object.assign({}, template); const doc2 = Object.assign({}, template); doc1.meta.version = 2; doc1.meta.tags.push('draft'); console.log(doc2.meta.version); // 2 - той самий об'єкт! console.log(doc2.meta.tags); // ['draft'] - той самий масив! ``` Обидва документи посилаються на один і той самий об'єкт `meta`, бо assign скопіював лише посилання. `structuredClone(template)` вирішує проблему. Для старіших середовищ `JSON.parse(JSON.stringify(template))` працює для простих даних, але функції втрачаються, `Date` перетворюється на рядок, а `undefined` зникає. ### Object.seal() для схеми форми ```javascript const formSchema = Object.seal({ username: '', email: '', role: 'viewer' }); formSchema.username = 'alice'; // Спрацює - значення можна змінювати formSchema.role = 'admin'; // Спрацює formSchema.password = '123'; // Не спрацює - форма (shape) зафіксована console.log(formSchema); // { username: 'alice', email: '', role: 'admin' } console.log(Object.isSealed(formSchema)); // true console.log(Object.isFrozen(formSchema)); // false ``` `Object.seal()` - правильний вибір, коли конфіг-об'єкт має відому структуру, яка не повинна змінюватися, але значення полів мають оновлюватися в рантаймі. Зустрічається рідше за freeze, але це саме той інструмент, коли freeze був би надто суворим. ### Edge case з дескрипторами та assign Це ловить досвідчених розробників, які вважають, що assign зберігає descriptor-прапори. ```javascript const source = Object.create(null, { id: { value: 42, writable: false, enumerable: true }, token: { get: () => 'secret', enumerable: true } }); const target = Object.assign({}, source); target.id = 100; // Спрацює - writable:false не скопіювався console.log(target.id); // 100 console.log(target.token); // 'secret' - геттер викликали, значення збережено const frozen = Object.freeze(target); frozen.id = 999; // Не спрацює - тепер freeze застосував writable:false console.log(frozen.id); // 100 ``` `Object.assign()` викликав геттер `token` на джерелі і зберіг рядок як звичайну data-властивість на `target`. Якщо потрібно скопіювати сам геттер, використовуй `Object.defineProperties(target, Object.getOwnPropertyDescriptors(source))`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.