Дослідіть патерни JavaScript Proxy для модифікації поведінки об'єктів. Дізнайтеся про валідацію, віртуалізацію, відстеження та інші передові техніки з прикладами коду.
Патерни JavaScript Proxy: опанування модифікацією поведінки об'єктів
Об'єкт JavaScript Proxy надає потужний механізм для перехоплення та налаштування фундаментальних операцій з об'єктами. Ця можливість відкриває двері до широкого спектра патернів проєктування та передових технік для керування поведінкою об'єктів. Цей вичерпний посібник досліджує різноманітні патерни Proxy, ілюструючи їх використання практичними прикладами коду.
Що таке JavaScript Proxy?
Об'єкт Proxy "обгортає" інший об'єкт (ціль) і перехоплює його операції. Ці операції, відомі як "пастки" (traps), включають пошук властивостей, присвоєння, перерахування та виклик функцій. Proxy дозволяє вам визначати власну логіку, яка виконуватиметься до, після або замість цих операцій. Основна концепція Proxy пов'язана з "метапрограмуванням", що дає змогу маніпулювати поведінкою самої мови JavaScript.
Основний синтаксис для створення Proxy:
const proxy = new Proxy(target, handler);
- target: Оригінальний об'єкт, для якого ви хочете створити проксі.
- handler: Об'єкт, що містить методи (пастки), які визначають, як Proxy перехоплює операції над цільовим об'єктом.
Поширені пастки Proxy
Об'єкт-обробник може визначати кілька пасток. Ось деякі з найчастіше використовуваних:
- get(target, property, receiver): Перехоплює доступ до властивості (напр.,
obj.property
). - set(target, property, value, receiver): Перехоплює присвоєння властивості (напр.,
obj.property = value
). - has(target, property): Перехоплює оператор
in
(напр.,'property' in obj
). - deleteProperty(target, property): Перехоплює оператор
delete
(напр.,delete obj.property
). - apply(target, thisArg, argumentsList): Перехоплює виклики функцій (коли ціль є функцією).
- construct(target, argumentsList, newTarget): Перехоплює оператор
new
(коли ціль є функцією-конструктором). - getPrototypeOf(target): Перехоплює виклики
Object.getPrototypeOf()
. - setPrototypeOf(target, prototype): Перехоплює виклики
Object.setPrototypeOf()
. - isExtensible(target): Перехоплює виклики
Object.isExtensible()
. - preventExtensions(target): Перехоплює виклики
Object.preventExtensions()
. - getOwnPropertyDescriptor(target, property): Перехоплює виклики
Object.getOwnPropertyDescriptor()
. - defineProperty(target, property, descriptor): Перехоплює виклики
Object.defineProperty()
. - ownKeys(target): Перехоплює виклики
Object.getOwnPropertyNames()
таObject.getOwnPropertySymbols()
.
Патерни Proxy та сценарії використання
Розглянемо деякі поширені патерни Proxy та їх застосування в реальних сценаріях:
1. Валідація
Патерн "Валідація" використовує Proxy для застосування обмежень на присвоєння властивостей. Це корисно для забезпечення цілісності даних.
const validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Вік не є цілим числом');
}
if (value < 0) {
throw new RangeError('Вік має бути невід\'ємним цілим числом');
}
}
// Стандартна поведінка для збереження значення
obj[prop] = value;
// Позначити успішне виконання
return true;
}
};
let person = {};
let proxy = new Proxy(person, validator);
proxy.age = 25; // Валідно
console.log(proxy.age); // Вивід: 25
try {
proxy.age = 'young'; // Генерує TypeError
} catch (e) {
console.log(e); // Вивід: TypeError: Вік не є цілим числом
}
try {
proxy.age = -10; // Генерує RangeError
} catch (e) {
console.log(e); // Вивід: RangeError: Вік має бути невід'ємним цілим числом
}
Приклад: Розглянемо платформу електронної комерції, де дані користувачів потребують валідації. Проксі може забезпечувати дотримання правил для віку, формату електронної пошти, складності пароля та інших полів, запобігаючи збереженню недійсних даних.
2. Віртуалізація (ліниве завантаження)
Віртуалізація, також відома як ліниве завантаження (lazy loading), відкладає завантаження ресурсномістких ресурсів доти, доки вони справді не знадобляться. Проксі може виступати як заповнювач для реального об'єкта, завантажуючи його лише при доступі до властивості.
const expensiveData = {
load: function() {
console.log('Завантаження ресурсномістких даних...');
// Симуляція тривалої операції (напр., отримання даних з бази)
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: 'Це ресурсномісткі дані'
});
}, 2000);
});
}
};
const lazyLoadHandler = {
get: function(target, prop) {
if (prop === 'data') {
console.log('Доступ до даних, завантаження за потреби...');
return target.load().then(result => {
target.data = result.data; // Зберегти завантажені дані
return result.data;
});
} else {
return target[prop];
}
}
};
const lazyData = new Proxy(expensiveData, lazyLoadHandler);
console.log('Початковий доступ...');
lazyData.data.then(data => {
console.log('Дані:', data); // Вивід: Дані: Це ресурсномісткі дані
});
console.log('Наступний доступ...');
lazyData.data.then(data => {
console.log('Дані:', data); // Вивід: Дані: Це ресурсномісткі дані (завантажено з кешу)
});
Приклад: Уявіть велику соціальну мережу з профілями користувачів, які містять численні деталі та пов'язані медіафайли. Негайне завантаження всіх даних профілю може бути неефективним. Віртуалізація за допомогою Proxy дозволяє спочатку завантажити базову інформацію профілю, а потім завантажувати додаткові деталі або медіаконтент лише тоді, коли користувач переходить до відповідних розділів.
3. Логування та відстеження
Проксі можна використовувати для відстеження доступу до властивостей та їх модифікацій. Це цінно для налагодження, аудиту та моніторингу продуктивності.
const logHandler = {
get: function(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value) {
console.log(`SET ${prop} to ${value}`);
target[prop] = value;
return true;
}
};
let obj = { name: 'Alice' };
let proxy = new Proxy(obj, logHandler);
console.log(proxy.name); // Вивід: GET name, Alice
proxy.age = 30; // Вивід: SET age to 30
Приклад: У застосунку для спільного редагування документів Proxy може відстежувати кожну зміну, внесену до вмісту документа. Це дозволяє створювати журнал аудиту, реалізувати функціонал скасування/повторення дії (undo/redo) та надавати статистику про внесок користувачів.
4. Представлення "тільки для читання"
Проксі можуть створювати представлення об'єктів "тільки для читання", запобігаючи випадковим змінам. Це корисно для захисту конфіденційних даних.
const readOnlyHandler = {
set: function(target, prop, value) {
console.error(`Неможливо встановити властивість ${prop}: об'єкт доступний тільки для читання`);
return false; // Позначити, що операція присвоєння не вдалася
},
deleteProperty: function(target, prop) {
console.error(`Неможливо видалити властивість ${prop}: об'єкт доступний тільки для читання`);
return false; // Позначити, що операція видалення не вдалася
}
};
let data = { name: 'Bob', age: 40 };
let readOnlyData = new Proxy(data, readOnlyHandler);
try {
readOnlyData.age = 41; // Генерує помилку
} catch (e) {
console.log(e); // Помилка не генерується, оскільки пастка 'set' повертає false.
}
try {
delete readOnlyData.name; // Генерує помилку
} catch (e) {
console.log(e); // Помилка не генерується, оскільки пастка 'deleteProperty' повертає false.
}
console.log(data.age); // Вивід: 40 (без змін)
Приклад: Розглянемо фінансову систему, де деякі користувачі мають доступ до інформації про рахунки лише для читання. Проксі можна використовувати, щоб заборонити цим користувачам змінювати баланс рахунку чи інші критичні дані.
5. Значення за замовчуванням
Проксі може надавати значення за замовчуванням для відсутніх властивостей. Це спрощує код і дозволяє уникнути перевірок на null/undefined.
const defaultValuesHandler = {
get: function(target, prop, receiver) {
if (!(prop in target)) {
console.log(`Властивість ${prop} не знайдено, повертаємо значення за замовчуванням.`);
return 'Значення за замовчуванням'; // Або будь-яке інше відповідне значення за замовчуванням
}
return Reflect.get(target, prop, receiver);
}
};
let config = { apiUrl: 'https://api.example.com' };
let configWithDefaults = new Proxy(config, defaultValuesHandler);
console.log(configWithDefaults.apiUrl); // Вивід: https://api.example.com
console.log(configWithDefaults.timeout); // Вивід: Властивість timeout не знайдено, повертаємо значення за замовчуванням. Значення за замовчуванням
Приклад: У системі керування конфігурацією Proxy може надавати значення за замовчуванням для відсутніх налаштувань. Наприклад, якщо у конфігураційному файлі не вказано тайм-аут з'єднання з базою даних, Proxy може повернути попередньо визначене значення за замовчуванням.
6. Метадані та анотації
Проксі можуть прикріплювати метадані або анотації до об'єктів, надаючи додаткову інформацію без зміни вихідного об'єкта.
const metadataHandler = {
get: function(target, prop, receiver) {
if (prop === '__metadata__') {
return { description: 'Це метадані для об\'єкта' };
}
return Reflect.get(target, prop, receiver);
}
};
let article = { title: 'Вступ до проксі', content: '...' };
let articleWithMetadata = new Proxy(article, metadataHandler);
console.log(articleWithMetadata.title); // Вивід: Вступ до проксі
console.log(articleWithMetadata.__metadata__.description); // Вивід: Це метадані для об'єкта
Приклад: У системі керування контентом Proxy може прикріплювати до статей метадані, такі як інформація про автора, дата публікації та ключові слова. Ці метадані можна використовувати для пошуку, фільтрації та категоризації контенту.
7. Перехоплення функцій
Проксі можуть перехоплювати виклики функцій, дозволяючи додавати логування, валідацію або іншу логіку попередньої чи подальшої обробки.
const functionInterceptor = {
apply: function(target, thisArg, argumentsList) {
console.log('Виклик функції з аргументами:', argumentsList);
const result = target.apply(thisArg, argumentsList);
console.log('Функція повернула:', result);
return result;
}
};
function add(a, b) {
return a + b;
}
let proxiedAdd = new Proxy(add, functionInterceptor);
let sum = proxiedAdd(5, 3); // Вивід: Виклик функції з аргументами: [5, 3], Функція повернула: 8
console.log(sum); // Вивід: 8
Приклад: У банківському застосунку Proxy може перехоплювати виклики функцій транзакцій, логуючи кожну транзакцію та виконуючи перевірки на шахрайство перед її виконанням.
8. Перехоплення конструкторів
Проксі можуть перехоплювати виклики конструкторів, дозволяючи налаштовувати створення об'єктів.
const constructorInterceptor = {
construct: function(target, argumentsList, newTarget) {
console.log('Створення нового екземпляра', target.name, 'з аргументами:', argumentsList);
const obj = new target(...argumentsList);
console.log('Створено новий екземпляр:', obj);
return obj;
}
};
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let ProxiedPerson = new Proxy(Person, constructorInterceptor);
let person = new ProxiedPerson('Alice', 28); // Вивід: Створення нового екземпляра Person з аргументами: ['Alice', 28], Створено новий екземпляр: Person { name: 'Alice', age: 28 }
console.log(person);
Приклад: У фреймворку для розробки ігор Proxy може перехоплювати створення ігрових об'єктів, автоматично призначаючи унікальні ідентифікатори, додаючи компоненти за замовчуванням та реєструючи їх в ігровому рушії.
Додаткові аспекти
- Продуктивність: Хоча проксі пропонують гнучкість, вони можуть створювати додаткове навантаження на продуктивність. Важливо проводити тестування та профілювання вашого коду, щоб переконатися, що переваги від використання проксі переважують витрати на продуктивність, особливо в критично важливих застосунках.
- Сумісність: Проксі є відносно новим доповненням до JavaScript, тому старіші браузери можуть їх не підтримувати. Використовуйте визначення наявності функцій (feature detection) або поліфіли для забезпечення сумісності зі старими середовищами.
- Відкличні проксі: Метод
Proxy.revocable()
створює проксі, який можна відкликати. Відкликання проксі запобігає перехопленню будь-яких подальших операцій. Це може бути корисним для цілей безпеки або керування ресурсами. - Reflect API: API Reflect надає методи для виконання стандартної поведінки пасток Proxy. Використання
Reflect
гарантує, що ваш код Proxy поводитиметься відповідно до специфікації мови.
Висновок
JavaScript Proxies надають потужний та універсальний механізм для налаштування поведінки об'єктів. Опанувавши різноманітні патерни Proxy, ви зможете писати більш надійний, підтримуваний та ефективний код. Незалежно від того, чи реалізуєте ви валідацію, віртуалізацію, відстеження чи інші передові техніки, проксі пропонують гнучке рішення для контролю доступу до об'єктів та маніпуляцій з ними. Завжди враховуйте наслідки для продуктивності та забезпечуйте сумісність з вашими цільовими середовищами. Проксі є ключовим інструментом в арсеналі сучасного розробника JavaScript, що відкриває потужні можливості метапрограмування.
Подальше дослідження
- Mozilla Developer Network (MDN): JavaScript Proxy
- Exploring JavaScript Proxies: Стаття у Smashing Magazine