Пастки (traps) у Proxy
Пастки (traps), це методи в обробнику об'єкта Proxy, які перехоплюють стандартні операції над об'єктом (читання, запис, видалення властивостей, виклик функції, оператор in тощо) і дозволяють перевизначити їхню поведінку. Простіше кажучи, пастки дають змогу втрутитися в роботу JavaScript і змінити те, як об'єкт себе поводить.
Теорія
TL;DR
- Пастки, це функції, які визначають у другому аргументі
Proxy, у так званому обробнику (handler). - Кожна пастка відповідає одній внутрішній операції:
get,set,has,deleteProperty,ownKeys,apply,constructта іншим. - Сигнатура пастки один в один збігається з однойменним методом
Reflect. - Усередині пастки стандартну поведінку викликають через
Reflect, а не через прямий доступ доtarget. - Якщо пастки для операції немає, операція виконується на
targetбез змін. - Пастки
applyіconstructпрацюють лише тоді, колиtarget, це функція.
Швидкий приклад
const user = { name: 'Maria' };
const proxy = new Proxy(user, {
get(target, property) {
console.log(`Reading property "${String(property)}"`);
return target[property];
}
});
console.log(proxy.name);
// Reading property "name"
// MariaТут пастка get перехоплює звернення до властивості, і перед поверненням значення можна зробити будь-що: логування, перевірку, валідацію, підміну значення.
Синтаксис і обробник
const proxy = new Proxy(target, handler);| Параметр | Опис |
|---|---|
target | Вихідний об'єкт, справжня «ціль» |
handler | Об'єкт з пастками, тобто методами, що перехоплюють дії |
Повний список основних пасток
| Пастка | Що перехоплює | Аналог |
|---|---|---|
get(target, prop, receiver) | Читання властивості (obj.prop) | obj[prop] |
set(target, prop, value, receiver) | Запис властивості (obj.prop = value) | obj[prop] = value |
has(target, prop) | Перевірку prop in obj | in |
deleteProperty(target, prop) | Видалення (delete obj[prop]) | delete |
ownKeys(target) | Отримання списку ключів (Object.keys, for...in) | Object.keys(obj) |
getOwnPropertyDescriptor(target, prop) | Запит дескриптора властивості | Object.getOwnPropertyDescriptor() |
defineProperty(target, prop, descriptor) | Визначення нової властивості | Object.defineProperty() |
getPrototypeOf(target) | Отримання прототипа | Object.getPrototypeOf() |
setPrototypeOf(target, proto) | Зміну прототипа | Object.setPrototypeOf() |
isExtensible(target) | Перевірку розширюваності | Object.isExtensible() |
preventExtensions(target) | Заборону додавати нові властивості | Object.preventExtensions() |
apply(target, thisArg, args) | Виклик функції | func() |
construct(target, args, newTarget) | Виклик через new | new func() |
Популярні пастки для об'єктів
set, контроль запису:
const user = { name: 'Maria' };
const proxy = new Proxy(user, {
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
target[prop] = value;
return true; // обов'язково повернути true
}
});
proxy.age = 25; // ok
proxy.age = 'twenty'; // TypeErrorhas, підміна оператора in:
const secret = { password: '1234' };
const proxy = new Proxy(secret, {
has(target, key) {
if (key === 'password') return false;
return key in target;
}
});
console.log('password' in proxy); // falseТак можна «ховати» чутливі поля від перевірок наявності.
ownKeys, приховування властивостей при обході:
const user = { name: 'Maria', password: '12345' };
const proxy = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(k => k !== 'password');
}
});
console.log(Object.keys(proxy)); // ["name"]Пастки для функцій: apply і construct
apply, перехоплення виклику функції:
function sum(a, b) {
return a + b;
}
const proxy = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`Called with arguments: ${args}`);
return Reflect.apply(target, thisArg, args);
}
});
console.log(proxy(2, 3));
// Called with arguments: 2,3
// 5construct, перехоплення виклику з new:
function User(name) {
this.name = name;
}
const proxy = new Proxy(User, {
construct(target, args) {
console.log('Creating a new user...');
return new target(...args);
}
});
const maria = new proxy('Maria');
// "Creating a new user..."Обидві пастки спрацьовують лише тоді, коли target, це функція: загорнути звичайний об'єкт і перехопити apply не вийде.
Як делегувати стандартну поведінку через Reflect
Щоб не ламати нативну поведінку, усередині пасток зазвичай викликають Reflect: він виконує «справжню» операцію під капотом.
const proxy = new Proxy(user, {
get(target, prop, receiver) {
console.log(`Reading ${String(prop)}`);
return Reflect.get(target, prop, receiver);
}
});Це гарантує, що все, чого ви не збиралися змінювати, лишиться таким, як за замовчуванням. Третій аргумент receiver тут не декоративний: він передає геттерам правильний this, а з простим target[prop] контекст губиться.
Де реально застосовують пастки
- Валідація і захист даних.
- Логування доступу до властивостей.
- Створення «віртуальних» властивостей, яких фізично немає в об'єкті.
- Реактивні системи (Vue, MobX).
- ORM та обгортки над API, де моделі підставляються динамічно.
- Метапрограмування і тестові моки.
| Термін | Що це |
|---|---|
| Пастка (trap) | Метод-обробник у Proxy, який перехоплює операцію з об'єктом |
| Handler | Об'єкт, що містить пастки |
| Reflect | Використовується всередині пасток для виклику стандартної поведінки |
| Target | Справжній об'єкт, над яким стоїть Proxy |
Типові помилки
- Не повертати булеве значення з
set,deleteProperty,has,defineProperty. Ці пастки зобов'язані повернутиtrueабоfalse;undefinedтрактується якfalse, і в strict mode операція впаде зTypeError. - Використовувати
target[prop]замістьReflect.get(target, prop, receiver). Так губитьсяreceiver, і геттери та успадковані властивості отримують неправильнийthis. - Забувати, що
propможе бутиSymbol. Вставка символьного ключа в шаблонний рядок безString(prop)кинеTypeError. - Порушувати інваріанти. Пастка не всесильна:
ownKeysмусить повернути всі неконфігуровані власні ключі, аgetне може віддати інше значення для неконфігурованої властивості зwritable: false. Інакше рушій кинеTypeError. - Чекати на
applyдля об'єкта. Пасткиapplyіconstructпрацюють тільки з функціями. - Вважати, що пастки перехоплюють усе. Внутрішні слоти (наприклад, у
Map,Set,Date) в обхід пасток звертаються до реального об'єкта, тому такі вбудовані типи черезProxyбез прив'язки методів ламаються.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.