Что такое Proxy в JavaScript
Proxy в JavaScript — мощный инструмент, который позволяет создать обертку вокруг объекта или функции (называемых целевым объектом). Эта обертка дает возможность перехватывать, контролировать и изменять базовые операции с этим объектом с помощью специальных методов, называемых ловушками (traps). Ловушки представляют собой функции, которые определяют, как должны обрабатываться различные типы операций — чтение свойств, запись значений, вызов методов и другие.
Предположим, у нас есть обычный объект:
let user = {
name: "Иван",
age: 30
};
Когда мы обращаемся к свойствам этого объекта или изменяем их, это происходит напрямую. Но что, если мы хотим добавить какую-то дополнительную логику при каждом обращении или изменении? Здесь на помощь приходит Proxy.
Proxy позволяет нам создать посредника между кодом и реальным объектом. Этот посредник может перехватывать различные операции:
- Чтение свойств объекта.
- Запись новых значений в свойства.
- Удаление свойств.
- Перебор свойств объекта.
- Вызов функций (если объект является функцией).
Простейший пример использования Proxy с JavaScript выглядит так:
let user = {
name: "Иван",
age: 30
};
let userProxy = new Proxy(user, {
get(target, property) {
console.log(`Кто-то пытается получить свойство ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`Кто-то пытается изменить свойство ${property} на значение ${value}`);
target[property] = value;
return true;
}
});
console.log(userProxy.name); // Выведет: "Кто-то пытается получить свойство name" и затем "Иван"
userProxy.age = 31; // Выведет: "Кто-то пытается изменить свойство age на значение 31"
А так можно переписать этот пример на TypeScript:
interface User {
name: string;
age: number;
}
const user: User = {
name: "Иван",
age: 30
};
const userProxy = new Proxy<User>(user, {
get(target: User, property: keyof User): any {
console.log(`Кто-то пытается получить свойство ${String(property)}`);
return target[property];
},
set(target: User, property: keyof User, value: any): boolean {
console.log(`Кто-то пытается изменить свойство ${String(property)} на значение ${value}`);
(target as any)[property] = value;
return true;
}
});
console.log(userProxy.name); // Выведет: "Кто-то пытается получить свойство name" и затем "Иван"
userProxy.age = 31; // Выведет: "Кто-то пытается изменить свойство age на значение 31"
Для чего можно использовать Proxy
Proxy предоставляет мощный механизм для управления поведением объектов на глубоком уровне. Вот несколько основных случаев использования:
- Валидация — можно использовать Proxy для проверки входных данных перед их присвоением свойствам объекта.
- Ленивая загрузка — Proxy позволяет отсрочить загрузку данных до того момента, когда они действительно нужны.
- Обработка ошибок — можно перехватывать исключения, возникающие при работе с объектом, и обрабатывать их соответствующим образом.
- Логирование — Proxy помогает легко добавить логирование всех операций с объектом без изменения самого объекта.
- Прокси-серверы и API — Proxy служит для создания прозрачных прокси-серверов или API, которые могут изменять поведение запросов и ответов.
Рассмотрим несколько примеров подробнее.
Автозаполнение свойств
Proxy может динамически заполнять свойства объекта при доступе к ним — так можно реализовать обработку по требованию или инициализацию сложных объектов.
В этом примере у нас есть объект профиля пользователя с именем, фамилией и полным именем, которое генерируется автоматически при обращении к нему:
type LazyProfile = {
firstName: string;
lastName: string;
fullName?: string;
};
let lazyProfileHandler: ProxyHandler<LazyProfile> = {
get: (target: LazyProfile, property: keyof LazyProfile) => {
if (property === "fullName" && !target[property]) {
target[property] = `${target.firstName} ${target.lastName}`;
}
return target[property];
}
};
let profile = new Proxy<LazyProfile>({ firstName: "Андрей", lastName: "Болконский" }, lazyProfileHandler);
console.log(profile.fullName); // Вывод: Андрей Болконский
const lazyProfileHandler = {
get: (target, property) => {
if (property === "fullName" && !target[property]) {
target[property] = `${target.firstName} ${target.lastName}`;
}
return target[property];
}
};
const profile = new Proxy({ firstName: "Андрей", lastName: "Болконский" }, lazyProfileHandler);
console.log(profile.fullName); // Вывод: Андрей Болконский
Подсчет количества операций
В этом примере с помощью Proxy подсчитывается число операций чтения свойств объекта — такой подход можно использовать для отладки, мониторинга или тестирования производительности приложения:
type Counter = {
[key: string]: any;
_getCount: number;
};
let countHandler: ProxyHandler<Counter> = {
get: (target: Counter, property: string | symbol, receiver: any) => {
if (property === "_getCount") {
return target[property];
}
target._getCount++;
return Reflect.get(target, property, receiver);
}
};
let counter: Counter = new Proxy({ a: 1, b: 2, _getCount: 0 }, countHandler);
counter.a;
counter.b;
console.log(counter._getCount); // Вывод: 2
const countHandler = {
get: (target, property, receiver) => {
if (property === "_getCount") {
return target[property];
}
target._getCount++;
return Reflect.get(target, property, receiver);
}
};
const counter = new Proxy({ a: 1, b: 2, _getCount: 0 }, countHandler);
counter.a;
counter.b;
console.log(counter._getCount); // Вывод: 2
Создание неизменяемых объектов
С помощью Proxy можно создавать неизменяемые объекты: любая попытка изменить свойства такого объекта после создания будет вызывать ошибку. В этом примере создается функция createImmutable
, которая принимает объект и возвращает его неизменяемую версию:
function createImmutable<T extends object>(obj: T): T {
return new Proxy(obj, {
set: (target, property, value) => {
console.log(`Попытка изменить свойство '${String(property)}' на значение ${value}`);
console.log("Ошибка: Этот объект неизменяем");
return false; // Возвращаем false, чтобы указать, что операция не удалась
}
});
}
const immutableObject = createImmutable({ name: "Иван", age: 30 });
console.log("Исходный объект:", immutableObject);
try {
immutableObject.age = 31;
} catch (error) {
console.log("Произошло исключение:", error.message);
}
try {
// @ts-ignore (используется для обхода проверки типов TypeScript)
immutableObject.newProperty = "Новое свойство";
} catch (error) {
console.log("Произошло исключение:", error.message);
}
console.log("Объект после попыток изменения:", immutableObject);
function createImmutable(obj) {
return new Proxy(obj, {
set: (target, property, value) => {
console.log(`Попытка изменить свойство '${String(property)}' на значение ${value}`);
console.log("Ошибка: Этот объект неизменяем");
return false; // Возвращаем false, чтобы указать, что операция не удалась
}
});
}
const immutableObject = createImmutable({ name: "Иван", age: 30 });
console.log("Исходный объект:", immutableObject);
try {
immutableObject.age = 31;
} catch (error) {
console.log("Произошло исключение:", error.message);
}
try {
immutableObject.newProperty = "Новое свойство";
} catch (error) {
console.log("Произошло исключение:", error.message);
}
console.log("Объект после попыток изменения:", immutableObject);
Создание текучего интерфейса с цепочкой вызовов методов
Текучий интерфейс позволяет вызывать методы объекта цепочкой:
type FluentPerson = {
setName(name: string): FluentPerson;
setAge(age: number): FluentPerson;
save(): void;
};
function FluentPerson(): FluentPerson {
let person: any = {};
const handler = {
get: (target: any, property: string | symbol) => {
if (property === "save") {
return () => { console.log(person); };
}
return (value: any) => {
person[property] = value;
return proxy;
};
}
};
const proxy = new Proxy({}, handler) as FluentPerson;
return proxy;
}
const person = FluentPerson();
person.setName("Онегин").setAge(30).save(); // Вывод: { setName: 'Онегин', setAge: 30 }
function FluentPerson() {
let person = {};
const handler = {
get: (target, property) => {
if (property === "save") {
return () => { console.log(person); };
}
return (value) => {
person[property] = value;
return proxy;
};
}
};
const proxy = new Proxy({}, handler);
return proxy;
}
const person = FluentPerson();
person.setName("Онегин").setAge(30).save(); // Вывод: {setName: 'Онегин', setAge: 30}
Умное кэширование
В этом примере данные извлекаются или вычисляются по требованию, а затем хранятся для быстрого последующего доступа:
function smartCache<T extends object>(obj: T, fetcher: (key: keyof T) => any): T {
const cache: Partial<T> = {};
return new Proxy(obj, {
get: (target, property: string | symbol) => {
if (!cache[property]) {
cache[property] = fetcher(property as keyof T);
}
return cache[property];
}
});
}
const userData = smartCache({ userId: 1 }, (prop) => {
console.log(`Получаем данные пользователя ${String(prop)}`);
return { name: "Евгений" }; // Симуляция получения данных
});
console.log(userData.userId); // Вывод: Получаем данные пользователя userId, { name: 'Евгений' }
function smartCache(obj, fetcher) {
const cache = {};
return new Proxy(obj, {
get: function(target, property) {
if (!cache[property]) {
cache[property] = fetcher(property);
}
return cache[property];
}
});
}
const userData = smartCache({ userId: 1 }, function(prop) {
console.log(`Получаем данные пользователя ${String(prop)}`);
return { name: "Евгений" }; // Симуляция получения данных
});
console.log(userData.userId); // Вывод: Получаем данные пользователя userId, { name: 'Евгений' }
Динамическая валидация свойств
Proxy можно использовать для создания объектов с динамической валидацией свойств — для обеспечения целостности данных и предотвращения ошибок:
let user = {
age: 25,
name: "Евгений",
email: "onegin@example.com"
};
let validator = {
set: (obj, prop, value) => {
if (prop === 'age') {
if (typeof value !== 'number' || value < 18 || value > 100) {
throw new Error("Возраст должен быть числом между 18 и 100.");
}
} else if (prop === 'name') {
if (typeof value !== 'string' || value.length < 2) {
throw new Error("Имя должно состоять хотя бы из двух символов.");
}
} else if (prop === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (typeof value !== 'string' || !emailRegex.test(value)) {
throw new Error("Неверный формат email.");
}
}
obj[prop] = value;
return true;
},
get: (obj, prop) => {
console.log(`Получаем свойство: ${String(prop)}`);
return obj[prop];
}
};
let userProxy = new Proxy(user, validator);
// Попытки изменения свойств объекта
console.log("--- Корректные данные ---");
try {
userProxy.age = 30;
console.log("Возраст изменен на:", userProxy.age);
userProxy.name = "Татьяна";
console.log("Имя изменено:", userProxy.name);
userProxy.email = "larina@example.com";
console.log("Email измененен на:", userProxy.email);
} catch (error) {
console.error("Возникла ошибка:", error.message);
}
console.log("\n--- Некорректные данные ---");
try {
userProxy.age = 15;
} catch (error) {
console.error("Ошибка изменения возраста:", error.message);
}
try {
userProxy.age = "thirty";
} catch (error) {
console.error("Ошибка изменения возраста:", error.message);
}
try {
userProxy.name = "A";
} catch (error) {
console.error("Ошибка изменения имени:", error.message);
}
try {
userProxy.email = "invalid-email";
} catch (error) {
console.error("Ошибка изменения email:", error.message);
}
console.log("\n--- Финальное состояние объекта ---");
console.log(userProxy);
let user = {
age: 25,
name: "Евгений",
email: "onegin@example.com"
};
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (typeof value !== 'number' || value < 18 || value > 100) {
throw new Error("Возраст должен быть числом между 18 и 100.");
}
} else if (prop === 'name') {
if (typeof value !== 'string' || value.length < 2) {
throw new Error("Имя должно состоять хотя бы из двух символов.");
}
} else if (prop === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (typeof value !== 'string' || !emailRegex.test(value)) {
throw new Error("Неверный формат email.");
}
}
obj[prop] = value;
return true;
},
get: function(obj, prop) {
console.log(`Получаем свойство: ${String(prop)}`);
return obj[prop];
}
};
let userProxy = new Proxy(user, validator);
// Попытки изменения свойств объекта
console.log("--- Корректные данные ---");
try {
userProxy.age = 30;
console.log("Возраст изменен на:", userProxy.age);
userProxy.name = "Татьяна";
console.log("Имя изменено:", userProxy.name);
userProxy.email = "larina@example.com";
console.log("Email измененен на:", userProxy.email);
} catch (error) {
console.error("Возникла ошибка:", error.message);
}
console.log("\n--- Некорректные данные ---");
try {
userProxy.age = 15;
} catch (error) {
console.error("Ошибка изменения возраста:", error.message);
}
try {
userProxy.age = "thirty";
} catch (error) {
console.error("Ошибка изменения возраста:", error.message);
}
try {
userProxy.name = "A";
} catch (error) {
console.error("Ошибка изменения имени:", error.message);
}
try {
userProxy.email = "invalid-email";
} catch (error) {
console.error("Ошибка изменения email:", error.message);
}
console.log("\n--- Финальное состояние объекта ---");
console.log(userProxy);
Отслеживание изменений
Proxy позволяет создать объекты, которые могут отслеживать изменения своих свойств и уведомлять об этих событиях. Этот подход особенно полезен в ситуациях, когда нужно логировать или реагировать на изменения в объекте:
function watchProperties(obj, onAccess, onChange) {
const handler = {
get: (target, property, receiver) => {
onAccess(`Свойство ${String(property)} было запрошено`);
return Reflect.get(target, property, receiver);
},
set: (target, property, value, receiver) => {
onChange(`Свойство ${String(property)} изменено на ${value}`);
return Reflect.set(target, property, value, receiver);
}
};
return new Proxy(obj, handler);
}
const book = {
title: "Лабиринт",
genre: "Триллер",
author: "Франк Тилье",
releaseYear: 2024,
price: 560
};
const watchedBook = watchProperties(book, console.log, console.log);
console.log(watchedBook.title); // Вывод: Свойство title было запрошено Лабиринт
console.log(watchedBook.genre); // Вывод: Свойство genre было запрошено Триллер
watchedBook.price = 570; // Вывод: Свойство price изменено на 570
console.log(watchedBook.author); // Вывод: Свойство author было запрошено Франк Тилье
watchedBook.releaseYear = 2022; // Вывод: Свойство releaseYear изменено на 2022
function watchProperties(obj, onAccess, onChange) {
const handler = {
get: (target, property, receiver) => {
onAccess(`Свойство ${String(property)} было запрошено`);
return Reflect.get(target, property, receiver);
},
set: (target, property, value, receiver) => {
onChange(`Свойство ${String(property)} было изменено на ${value}`);
return Reflect.set(target, property, value, receiver);
}
};
return new Proxy(obj, handler);
}
const book = {
title: "Лабиринт",
genre: "Триллер",
author: "Франк Тилье",
releaseYear: 2024,
price: 560
};
const watchedBook = watchProperties(book, console.log, console.log);
console.log(watchedBook.title); // Вывод: Свойство title было запрошено Лабиринт
console.log(watchedBook.genre); // Вывод: Свойство genre было запрошено Триллер
watchedBook.price = 570; // Вывод: Свойство price изменено на 570
console.log(watchedBook.author); // Вывод: Свойство author было запрошено Франк Тилье
watchedBook.releaseYear = 2022; // Вывод: Свойство releaseYear изменено на 2022
Недостатки использования Proxy
Хотя Proxy позволяет реализовать массу полезных и удобных функций, несколько недостатков у этого метода есть:
- Производительность. Использование Proxy может снижать производительность, особенно при частых операциях, потому что каждая операция с проксированным объектом должна проходить через обработчик.
- Сложность. С большой мощью приходит большая сложность. Некорректное использование Proxy может привести к трудноустранимым проблемам и проблемам с поддержкой кода.
- Совместимость. Proxy невозможно полифилить для старых браузеров, которые не поддерживают функции ES6, поэтому не стоит их использовать в средах, требующих широкой совместимости.
В заключение
Proxy в JavaScript, особенно при использовании с TypeScript, предлагает гибкий способ взаимодействия с объектами: позволяет реализовать валидацию, наблюдение, привязки, кэширование, цепной вызов методов и многие другие удобные функции. Независимо от того, создаете ли вы сложные пользовательские интерфейсы, разрабатываете игры или работаете над серверной логикой, понимание и использование Proxy предоставит вам более глубокий уровень контроля и гибкости.
Хотите освоить современную веб-разработку и создать свой первый интернет-магазин? Курс Frontend Basic от Poglib Academy предлагает 26 видеоуроков и 28 практических заданий, которые помогут вам изучить HTML, CSS, JavaScript и React.js за 2 месяца, даже если вы начинаете с нуля.
Комментарии