Что такое Proxy в JavaScript — и почему в консоли написано Proxy(Object)

Ты делаешь приложение на Vue. Пишешь state.count++ — и цифра на экране обновилась. Ты не вызывал никакой setState, не просил перерисовку. Просто поменял поле объекта.
Потом ты пишешь console.log(state) и видишь странное:
Proxy(Object) { count: 1 }
Не { count: 1 }, а какой-то Proxy. Вот он и есть фокус. Это не твой объект, а его вахтёр.
Что такое Proxy
Proxy — это объект-посредник, который встаёт перед настоящим объектом и перехватывает всё, что с ним делают: чтение поля, запись, удаление, проверку in.
Ты обращаешься к прокси — а он решает, что ответить. Может передать дальше как есть. Может что-то записать в журнал. Может отказать.
С сетевым прокси-сервером идея та же: посредник между тобой и тем, к кому ты идёшь. Только тут всё происходит внутри одной программы.
Как он работает
Создаётся так: new Proxy(цель, обработчик). Цель — обычный объект. Обработчик — объект с «ловушками», функциями на каждое действие.
const user = { name: "Аня" };
const watched = new Proxy(user, {
get(target, key) {
console.log("читают", key);
return target[key];
},
set(target, key, value) {
console.log("пишут", key, "=", value);
target[key] = value;
return true;
},
});
watched.name; // читают name
watched.age = 20; // пишут age = 20
Код, который работает с watched, даже не знает о посреднике. Для него это просто объект. Но каждое касание проходит через твои функции.
Ловушек больше десятка: get, set, has, deleteProperty, ownKeys и другие. Начинать хватает первых двух.
Где ты его уже используешь
Реактивность Vue 3. reactive() возвращает Proxy. Ловушка get запоминает: «этот компонент читал count». Ловушка set говорит: «count поменяли — перерисуй всех, кто читал». Отсюда и магия state.count++.
Во Vue 2 Proxy ещё не использовали, и было больно. Новое поле, добавленное после создания объекта, Vue просто не замечал. Приходилось писать Vue.set(obj, "key", value). Proxy эту дыру закрыл: он ловит даже поля, которых ещё не было.
Immer — библиотека, которой пишут неизменяемые обновления как обычные мутации. Внутри produce ты меняешь черновик-прокси, а Immer записывает изменения и собирает новый объект.
Валидация. Ловушка set может проверить значение и бросить ошибку: «возраст должен быть числом». Удобно для конфигов и отладки.
Для тебя вывод простой: если состояние «само» обновляет экран — где-то рядом почти наверняка Proxy. Сравни с состоянием в React, где тот же эффект требует явного setState.
Собери мини-реактивность за 10 строк
Чтобы магия Vue перестала быть магией, собери её урезанную версию сам:
function reactive(obj, onChange) {
return new Proxy(obj, {
set(target, key, value) {
target[key] = value;
onChange(key, value);
return true;
},
});
}
const state = reactive({ count: 0 }, () => {
document.querySelector("#count").textContent = state.count;
});
state.count++; // число на странице обновилось само
Вот и весь принцип. Ты меняешь поле — ловушка set узнаёт об этом и обновляет экран. Настоящий Vue умнее: он помнит, кто читал какое поле, и перерисовывает только их. Но скелет тот же.
Три ловушки, где он ломается
1. Прокси — не исходный объект. reactive(obj) === obj даст false. Сравниваешь объекты через === или кладёшь в Set — получаешь два разных объекта. Во Vue для этого есть toRaw().
2. Деструктуризация теряет связь. const { count } = reactive(state) — и count уже просто число. Посредника у числа нет, перехватывать нечего. Экран перестанет обновляться. Документация Vue прямо об этом предупреждает.
3. Map, Set и приватные поля. Попробуй:
const p = new Proxy(new Map(), {});
p.set("a", 1); // TypeError: ... called on incompatible receiver
Методы Map ищут внутренние данные у самого Map, а им подсунули прокси. С приватными полями класса #x та же беда. Vue и Immer обходят это специальным кодом, а самодельный Proxy — нет.
И бонус: structuredClone(proxy) падает с DataCloneError. Прокси нельзя клонировать. Сначала достань сырой объект.
Proxy медленный?
Медленнее прямого доступа: каждое касание — вызов функции. Для состояния интерфейса это незаметно. В горячем цикле на миллионы итераций лучше работать с сырым объектом.
Чем Proxy отличается от геттера и сеттера?
Геттер get name() вешается на одно заранее известное поле. Proxy перехватывает любое поле, даже то, которого ещё нет. Поэтому Vue 3 и перешёл на него.
Можно ли достать исходный объект из Proxy?
Встроенного способа нет — это сделано специально. Библиотеки дают свои: toRaw() во Vue, original() и current() в Immer.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





