Skip to main content

Об'єкт Proxy

Proxy, це обгортка над об'єктом, яка дозволяє перехоплювати (intercept) операції над ним. Простіше кажучи, Proxy дає можливість стати між вашим кодом і об'єктом, щоб контролювати, що відбувається під час звернення до властивостей, їх зміни та інших дій.

Теорія

TL;DR

  • new Proxy(target, handler) створює обгортку над вихідним об'єктом.
  • handler, це об'єкт із пастками (traps), кожна з яких перехоплює одну операцію.
  • Основні пастки: get, set, has, deleteProperty, ownKeys, apply, construct.
  • Пастки set і deleteProperty мають повертати true, інакше в strict mode буде TypeError.
  • Reflect містить методи, що відповідають кожній пастці, і виконує поведінку за замовчуванням.
  • Типові застосування: валідація, логування, значення за замовчуванням, захист властивостей, реактивність Vue 3, i18n з fallback.

Швидкий приклад

javascript
const user = { name: 'Tim', age: 25 }; const proxy = new Proxy(user, { get(target, prop) { console.log(`Читання властивості "${prop}"`); return target[prop]; }, set(target, prop, value) { console.log(`Зміна "${prop}" на "${value}"`); target[prop] = value; return true; // обов'язково повернути true, щоб не було помилки } }); console.log(proxy.name); // Читання властивості "name" -> "Tim" proxy.age = 26; // Зміна "age" на "26"

Синтаксис

javascript
const proxy = new Proxy(target, handler);
ПараметрОпис
targetвихідний об'єкт, над яким будується проксі
handlerоб'єкт із пастками (traps), що перехоплюють операції

Проксі не копіює об'єкт: усі операції в підсумку доходять до target, якщо пастка сама їх туди пропустить. Якщо пастки для операції немає, вона виконується так, ніби Proxy не існує.

Практичні приклади пасток

Валідація під час запису. Пастка set перевіряє тип і не дає покласти в об'єкт сміття.

javascript
const user = new Proxy({}, { set(target, prop, value) { if (prop === 'age' && typeof value !== 'number') { throw new TypeError('Вік має бути числом'); } target[prop] = value; return true; } }); user.age = 30; // ок user.age = 'тридцять'; // TypeError

Значення за замовчуванням. Пастка get віддає підставне значення для властивостей, яких немає.

javascript
const config = new Proxy({}, { get(target, prop) { return prop in target ? target[prop] : '<не задано>'; } }); console.log(config.host); // <не задано> config.host = 'localhost'; console.log(config.host); // localhost

Захист від видалення. Пастка deleteProperty забороняє прибрати ключове поле.

javascript
const secure = new Proxy({ id: 1, name: 'Admin' }, { deleteProperty(target, prop) { if (prop === 'id') { console.log('Видалення заборонено!'); return false; } delete target[prop]; return true; } }); delete secure.name; // видалиться delete secure.id; // Видалення заборонено!

Reflect і поведінка за замовчуванням

Reflect, це набір методів, що відповідають усім пасткам Proxy. Він дозволяє викликати поведінку за замовчуванням, коли вона вам потрібна.

javascript
function createLoggedObject(obj) { return new Proxy(obj, { get(target, prop, receiver) { console.log(`Доступ до властивості "${prop}"`); return Reflect.get(target, prop, receiver); // стандартна поведінка } }); } const user = createLoggedObject({ name: 'Tim' }); console.log(user.name); // Доступ до властивості "name"

Якщо не використовувати Reflect, довелося б вручну писати target[prop], а це може зламати поведінку успадкування і this: геттер у прототипі отримає неправильний receiver і читатиме поля не з проксі, а з target.

Доступні пастки (traps)

ПасткаЩо перехоплюєАналог Reflect
getЧитання властивостіReflect.get()
setЗапис властивостіReflect.set()
hasПеревірку prop in objReflect.has()
deletePropertyВидалення delete obj[prop]Reflect.deleteProperty()
ownKeysОтримання всіх ключів (Object.keys, for...in)Reflect.ownKeys()
getOwnPropertyDescriptorДескриптор властивостіReflect.getOwnPropertyDescriptor()
definePropertyСтворення або зміну властивостіReflect.defineProperty()
preventExtensionsЗаборону додавати нові властивостіReflect.preventExtensions()
isExtensibleПеревірку можливості розширенняReflect.isExtensible()
getPrototypeOfОтримання прототипуReflect.getPrototypeOf()
setPrototypeOfЗміну прототипуReflect.setPrototypeOf()
applyВиклик функціїReflect.apply()
constructВиклик через newReflect.construct()

Проксіювати можна не лише звичайний об'єкт, а й функцію, і тоді працюють пастки apply та construct:

javascript
function sum(a, b) { return a + b; } const proxySum = new Proxy(sum, { apply(target, thisArg, args) { console.log(`Виклик функції з аргументами: ${args}`); return Reflect.apply(target, thisArg, args); } }); console.log(proxySum(2, 3)); // Виклик функції з аргументами: 2,3 -> 5

Де Proxy справді корисний

СценарійПриклад використання
Валідація данихПеревірка типів під час запису
Налагодження і логуванняПерехоплення get та set для аналізу
Реактивність (Vue 3)Відстеження змін властивостей
Захист і обмеженняЗаборона видаляти чи змінювати властивості
Локалізація і доступ за ключемi18n, fallback-дані
API-обгортки«Розумні» динамічні об'єкти, наприклад REST API на льоту

Приклад динамічної структури для i18n: замість помилки або undefined користувач бачить сам ключ, а перекладачі одразу помічають, чого бракує.

javascript
const dictionary = { hello: 'Привіт', world: 'світ' }; const i18n = new Proxy(dictionary, { get(target, key) { if (key in target) return target[key]; return `[${key}]`; // підставимо ключ замість перекладу } }); console.log(i18n.hello); // Привіт console.log(i18n.missingKey); // [missingKey]

Короткий підсумок термінів:

ТермінОпис
ProxyОб'єкт, що перехоплює операції з іншим об'єктом
targetВихідний об'єкт (ціль)
handlerОб'єкт із пастками (методами-перехоплювачами)
trapМетод, що перехоплює певну операцію (get, set тощо)
ReflectДозволяє викликати стандартну поведінку за замовчуванням

Типові помилки

  • Забути return true в set або deleteProperty. Пастка має повідомити рушію, що операція вдалася. У strict mode, а модулі завжди strict, повернення false або undefined призводить до TypeError.
  • Писати target[prop] замість Reflect.get(target, prop, receiver). Втрачається receiver, тому геттери з прототипу отримають неправильний this.
  • Вважати, що проксі змінює сам об'єкт. Пастки спрацьовують тільки для звернень через проксі. Якщо десь залишилося посилання на target, усі перевірки обходяться.
  • Порушувати інваріанти об'єкта. Для неконфігурованої властивості пастка get не може повернути інше значення, а для нерозширюваного об'єкта ownKeys не може вигадати нові ключі: рушій кине TypeError.
  • Ставити проксі на все підряд. Кожна операція проходить через ваш код, тому проксі помітно повільніший за звичайний об'єкт у гарячому циклі.
  • Очікувати, що Proxy можна поліфілити. Перехоплення реалізовано на рівні рушія, тому для старих середовищ без підтримки замінника немає.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.