Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке Proxy Object в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Proxy object** - це конструктор, який обгортає цільовий об'єкт і перехоплює операції над ним через trap-методи в handler. ```javascript const proxy = new Proxy({ name: 'Alice' }, { get(target, prop, receiver) { console.log(`Reading ${prop}`); return Reflect.get(target, prop, receiver); } }); proxy.name; // Reading name -> Alice ``` **Ключове:** на відміну від `Object.defineProperty`, Proxy перехоплює всі операції над будь-якою властивістю, включно з тими, яких ще не існує.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Proxy object** - це вбудований конструктор JavaScript, який обгортає цільовий об'єкт і перехоплює операції над ним через handler з опціональними trap-методами. ## Теорія ### TL;DR - Уяви прохідну охорону: кожне читання, запис чи видалення проходить через твою trap-функцію перш ніж потрапити до цільового об'єкта - Головна різниця від `Object.defineProperty`: Proxy перехоплює ВСІ операції над БУДЬ-ЯКОЮ властивістю, навіть тими, що ще не існують; defineProperty працює тільки зі статично відомими ключами - `Proxy.revocable()` повертає `{proxy, revoke}`; після виклику `revoke()` кожна операція через цей proxy одразу кидає помилку - V8-бенчмарки показують, що Proxy в 2-5 разів повільніший за прямий доступ до властивостей; не використовуй у tight loops - Завжди передавай `receiver` у методи `Reflect` всередині trap, щоб зберегти правильний контекст `this` ### Швидкий приклад ```javascript const user = { name: 'Alice', age: 25 }; const proxy = new Proxy(user, { get(target, prop, receiver) { console.log(`Accessing ${prop}`); return Reflect.get(target, prop, receiver); // зберігаємо receiver для коректного this } }); console.log(proxy.name); // Accessing name -> Alice proxy.age; // Accessing age ``` Третій аргумент `receiver` важливий, коли методи використовують `this`. Без нього виклики через ланцюжок прототипів ламаються так, що важко зрозуміти чому. ### Proxy проти Object.defineProperty `Object.defineProperty` патчить одну конкретну властивість на момент написання коду. Він не перехоплює оператор `in`, `for...in`, `delete` або властивості, додані пізніше. Proxy працює на рівні об'єкта: один handler покриває всі операції над target, включно з динамічними ключами та майбутніми властивостями. Саме через це Vue 3 замінив підхід на основі `Object.defineProperty` (з Vue 2) на Proxy-реактивність. Старий підхід не бачив присвоювань за індексом у масивах і нових властивостей. Proxy ловить обидва випадки. ### Коли використовувати - Логування кожного читання та запису під час розробки: trap `get` і `set` - Валідація вхідних даних для об'єктів, структура яких змінюється під час виконання: trap `set` - Read-only обгортка над об'єктом: trap `set` і `deleteProperty` кидають помилку - Тимчасовий доступ до чутливих даних: `Proxy.revocable()` з `setTimeout` на `revoke` - Реактивний стан у Vue 3 або MobX Одне спостереження з продакшену: Proxy-валідація, яка випадково опинилась всередині циклу рендеру React, викликала помітні просідання FPS. Профайлер одразу показав причину, але це хороший нагадувач: накладні витрати Proxy ростуть лінійно з кількістю викликів. ### Як JavaScript-рушій обробляє Proxy V8 зберігає прихований слот handler у кожному екземплярі Proxy. Коли ти читаєш `proxy.prop`, V8 перевіряє, чи є в handler trap `get`. Якщо є, викликає твою функцію з `(target, prop, receiver)`. Якщо trap відсутній, V8 передає виклик до `Reflect.get(target, prop, receiver)` як поведінку за замовчуванням. Та сама логіка діє для `set`, `deleteProperty`, `has`, `ownKeys`, `apply`, `construct` та інших фундаментальних операцій зі специфікації. Revocable proxy (відкличний proxy) зберігає окрему функцію `revoke`, яка обнуляє слот handler при виклику. Після цього будь-яка операція через proxy одразу кидає `TypeError` без можливості відновлення. ### Типові помилки **1. Використання змінної proxy всередині власного trap** ```javascript // Неправильно: p - це сам proxy, тому p[prop] знову запускає get trap const p = new Proxy({}, { get(target, prop) { return p[prop]; // нескінченна рекурсія } }); p.x; // RangeError: Maximum call stack size exceeded ``` ```javascript // Правильно: використовуй target, а не змінну proxy const p = new Proxy({}, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } }); ``` **2. Пропущений аргумент receiver ламає виклики методів** ```javascript // Неправильно: без receiver `this` не вказуватиме на proxy у спадкованих методах const p = new Proxy(obj, { get(target, prop) { return target[prop]; } }); ``` ```javascript // Правильно: передай receiver, щоб зберегти `this` через ланцюжок прототипів const p = new Proxy(obj, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } }); ``` **3. Пряма мутація target всередині trap обходить set trap** ```javascript // Неправильно: пряма мутація target не запускає set trap const p = new Proxy({ count: 0 }, { get(target, prop) { target.count++; // set trap не спрацьовує return target[prop]; } }); ``` Якщо потрібно записати значення з trap, використовуй `Reflect.set(target, prop, value, receiver)`, щоб повний ланцюжок trap залишався активним. **4. Порівняння proxy з оригіналом через ===** ```javascript const original = { x: 1 }; const proxy = new Proxy(original, {}); console.log(proxy === original); // false - це різні об'єкти ``` Proxy створює окрему обгортку. Строге порівняння завжди повертає `false`. Зберігай посилання на оригінал окремо, якщо потрібно порівнювати ідентичність. **5. Забутий виклик revoke** ```javascript const secret = { apiKey: 'xyz-123' }; const { proxy, revoke } = Proxy.revocable(secret, {}); // Без revoke() proxy тримає secret доступним нескінченно setTimeout(revoke, 1000); console.log(proxy.apiKey); // xyz-123 (до revoke) // Після revoke: TypeError: Cannot perform 'get' on a proxy that has been revoked ``` ### Де зустрічається в реальних проектах - **Vue 3**: замінив `Object.defineProperty` на Proxy для реактивного стану, щоб відловлювати мутації масивів і нові властивості - **MobX**: observable-об'єкти використовують Proxy trap для відстеження читань і записів та запуску реакцій - **Immer**: обгортає чорновик стану в Proxy, щоб ти міг писати мутації, які виробляють незмінні оновлення - **Zustand**: використовує Proxy для інспекції змін стану в devtools - **Node.js vm2**: пісочниця запускає код всередині відкличних Proxy, що обмежують доступ до об'єктів хоста ### Follow-up питання **Q:** Яка повна сигнатура trap `get`? **A:** `get(target, property, receiver)`. `target` - обгорнутий об'єкт, `property` - ключ, до якого звертаються, `receiver` - сам proxy або об'єкт, що ініціював пошук через ланцюжок прототипів. **Q:** Як Proxy перехоплює цикл `for...in`? **A:** Через trap `ownKeys`. Поверни порожній масив і `for...in` не побачить жодної властивості. Без trap цикл працює звично з target. **Q:** Що відбувається після виклику `revoke()`? **A:** Слот handler обнулюється. Кожна наступна операція через proxy одразу кидає `TypeError`. Відновити proxy неможливо. **Q:** Чому Proxy повільніший за прямий доступ до властивостей? **A:** Кожна перехоплена операція проходить через додатковий виклик функції та внутрішній пошук handler у V8. Накладні витрати складають приблизно 2-5x порівняно з прямим читанням властивості. У звичайній логіці додатку це не відчутно; у hot paths відчутно. **Q:** Як реалізувати глибокий (deep) валідаційний Proxy? (рівень senior) **A:** У trap `set` перевіряй, чи є вхідне значення об'єктом або масивом. Якщо так, перед присвоєнням обгорни його ще одним Proxy і рекурсивно повтори логіку. Тоді кожен запис на будь-якій глибині проходить валідацію, а не тільки верхній рівень. ## Приклади ### Базовий: логування доступу до властивостей ```javascript const config = { debug: true, timeout: 3000 }; const trackedConfig = new Proxy(config, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); console.log(`[config] read "${prop}" = ${value}`); return value; }, set(target, prop, value, receiver) { console.log(`[config] set "${prop}" = ${value}`); return Reflect.set(target, prop, value, receiver); } }); trackedConfig.timeout; // [config] read "timeout" = 3000 trackedConfig.debug = false; // [config] set "debug" = false ``` Обидва trap делегують до `Reflect` з повною сигнатурою. Target залишається чистим; всі побічні ефекти живуть у handler. ### Середній: валідація стану в рантаймі Цей патерн схожий на те, що Zustand використовує у своєму devtools middleware. ```javascript const state = { todos: [{ id: 1, text: 'Buy milk', done: false }] }; const validatedState = new Proxy(state, { set(target, prop, value, receiver) { if (prop === 'todos') { if (!Array.isArray(value)) throw new TypeError('todos must be an array'); value.forEach(todo => { if (typeof todo.text !== 'string') throw new TypeError('todo.text must be a string'); }); } return Reflect.set(target, prop, value, receiver); } }); validatedState.todos = [{ id: 2, text: 'Walk dog', done: false }]; // OK validatedState.todos = [{ id: 3, text: 123, done: false }]; // TypeError: todo.text must be a string ``` Валідація запускається при кожному присвоєнні `todos`. Оригінальний об'єкт `state` напряму не чіпається. ### Просунутий: відкличний доступ до чутливих даних ```javascript function createTemporaryAccess(data, durationMs) { const { proxy, revoke } = Proxy.revocable(data, { get(target, prop, receiver) { console.log(`[access] reading ${String(prop)}`); return Reflect.get(target, prop, receiver); } }); setTimeout(revoke, durationMs); return proxy; } const credentials = createTemporaryAccess({ apiKey: 'xyz-123' }, 2000); console.log(credentials.apiKey); // [access] reading apiKey -> xyz-123 setTimeout(() => { try { console.log(credentials.apiKey); } catch (e) { console.log(e.message); // Cannot perform 'get' on a proxy that has been revoked } }, 3000); ``` Після `revoke()` змінна `credentials` все ще існує в пам'яті, але будь-яка операція через неї кидає помилку. Оригінальний об'єкт `data` стає доступним для збирача сміття, якщо немає інших посилань на нього.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.