Об'єкт 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.
Швидкий приклад
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"Синтаксис
const proxy = new Proxy(target, handler);| Параметр | Опис |
|---|---|
target | вихідний об'єкт, над яким будується проксі |
handler | об'єкт із пастками (traps), що перехоплюють операції |
Проксі не копіює об'єкт: усі операції в підсумку доходять до target, якщо пастка сама їх туди пропустить. Якщо пастки для операції немає, вона виконується так, ніби Proxy не існує.
Практичні приклади пасток
Валідація під час запису. Пастка set перевіряє тип і не дає покласти в об'єкт сміття.
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 віддає підставне значення для властивостей, яких немає.
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 забороняє прибрати ключове поле.
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. Він дозволяє викликати поведінку за замовчуванням, коли вона вам потрібна.
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 obj | Reflect.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 | Виклик через new | Reflect.construct() |
Проксіювати можна не лише звичайний об'єкт, а й функцію, і тоді працюють пастки apply та construct:
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 користувач бачить сам ключ, а перекладачі одразу помічають, чого бракує.
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можна поліфілити. Перехоплення реалізовано на рівні рушія, тому для старих середовищ без підтримки замінника немає.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.