🔧 Proxy в JavaScript и TypeScript: 7 способов использования

Объект Proxy в JavaScript/TypeScript – суперполезный инструмент, который открывает множество возможностей для управления и манипуляции объектами и функциями. Рассмотрим несколько практических примеров использования Proxy для кэширования, логирования, динамической валидации и вызова методов цепочкой.
🔧 Proxy в JavaScript и TypeScript: 7 способов использования

Что такое 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 может динамически заполнять свойства объекта при доступе к ним — так можно реализовать обработку по требованию или инициализацию сложных объектов.

В этом примере у нас есть объект профиля пользователя с именем, фамилией и полным именем, которое генерируется автоматически при обращении к нему:

TypeScript
        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);  // Вывод: Андрей Болконский
    
JavaScript
        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 подсчитывается число операций чтения свойств объекта — такой подход можно использовать для отладки, мониторинга или тестирования производительности приложения:

TypeScript
        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
    
JavaScript
        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, которая принимает объект и возвращает его неизменяемую версию:

TypeScript
        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);
    
JavaScript
        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);
    

Создание текучего интерфейса с цепочкой вызовов методов

Текучий интерфейс позволяет вызывать методы объекта цепочкой:

TypeScript
        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 }
    
JavaScript
        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}
    

Умное кэширование

В этом примере данные извлекаются или вычисляются по требованию, а затем хранятся для быстрого последующего доступа:

TypeScript
        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: 'Евгений' }
    
JavaScript
        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 можно использовать для создания объектов с динамической валидацией свойств — для обеспечения целостности данных и предотвращения ошибок:

TypeScript
        let user = {
  age: 25,
  name: "Евгений",
  email: "[email protected]"
};

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 = "[email protected]";
  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);
    
JavaScript
        let user = {
  age: 25,
  name: "Евгений",
  email: "[email protected]"
};

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 = "[email protected]";
  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 позволяет создать объекты, которые могут отслеживать изменения своих свойств и уведомлять об этих событиях. Этот подход особенно полезен в ситуациях, когда нужно логировать или реагировать на изменения в объекте:

TypeScript
        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
    
JavaScript
        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 месяца, даже если вы начинаете с нуля.

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик на Go в Еду
Москва, по итогам собеседования
Go Team Lead
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ