Что такое…

Что такое 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.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →