Что такое Proxy и Reflect в JavaScript?

SeniorJavaScript · Frontend·Обновлено 7 июля 2026
Коротко
Proxy — объект-обёртка, перехватывающий операции над другим объектом через ловушки (get, set, has и др.). Reflect — вспомогательный объект со статическими методами, зеркалирующими операции над объектами, который обычно используется внутри ловушек Proxy для вызова поведения по умолчанию.

Proxy

Проксирование — это паттерн, при котором один объект (proxy) оборачивает другой (target) и перехватывает обращения к нему. Класс Proxy появился в ES2015 и позволяет определить кастомное поведение для фундаментальных операций: чтение/запись свойств, вызов функции, проверка существования ключа и т.д.

Базовый синтаксис

const proxy = new Proxy(target, handler);
  • target — исходный объект (может быть любым объектом, массивом, функцией)
  • handler — объект с ловушками (traps)

Ловушки (traps)

Каждая ловушка соответствует внутреннему методу (Internal Method) спецификации:

Ловушка Перехватывает
get Чтение свойства
set Запись свойства
has Оператор in
deleteProperty Оператор delete
apply Вызов функции
construct Оператор new
ownKeys Object.keys, for...in
defineProperty Object.defineProperty
getPrototypeOf Object.getPrototypeOf

Reflect

Reflect — встроенный объект (не конструктор), методы которого один-в-один соответствуют внутренним методам JS-движка. Главные причины его существования:

  1. Дефолтное поведение в ловушках — вместо того чтобы дублировать логику движка, вызываем Reflect.get(target, prop, receiver) и получаем поведение «как будто прокси нет».
  2. Правильная передача receiver — при наследовании receiver — это реальный объект, на котором произошло обращение (а не target). Использование Reflect гарантирует корректный this в геттерах.
  3. Единый APIReflect.ownKeys объединяет Object.getOwnPropertyNames + Object.getOwnPropertySymbols.

Invariants (инварианты)

Даже с Proxy движок защищает ряд правил. Например, если исходное свойство non-writable и non-configurable, ловушка get не может вернуть другое значение — выбросится TypeError. Поэтому корректная реализация ловушек через Reflect автоматически соблюдает инварианты.

Типичные применения

  • Валидация данных — проверка типов при set
  • Реактивность — основа Vue 3 Reactivity (заменила Object.defineProperty)
  • Логирование/трассировка — прозрачный мониторинг обращений
  • Виртуальные объекты — ORM, «ленивые» свойства, мемоизация
  • Защита от записи — read-only обёртки

Ограничения

  • Proxy нельзя снять с объекта (target всегда доступен, если есть ссылка)
  • Не работает с примитивами
  • Некоторые встроенные структуры (WeakMap, Date) требуют особой обработки, потому что хранят [[Slots]]

Что хочет услышать интервьюер

Понимание разницы между target и proxy, а также зачем нужен handler с ловушками

Знание ключевых ловушек (get, set, apply, construct) и их сигнатур

Объяснение роли Reflect и почему важно передавать receiver в Reflect.get/set

Осведомлённость об инвариантах спецификации и почему «наивная» реализация ловушек может бросить TypeError

Реальные сценарии использования: реактивность (Vue 3), валидация, мемоизация или защищённые объекты

Пример: Валидация типов через Proxy + Reflect

interface User {
  name: string;
  age: number;
}

function createValidated<T extends object>(target: T): T {
  return new Proxy(target, {
    set(t, prop, value, receiver) {
      if (prop === 'age' && typeof value !== 'number') {
        throw new TypeError(`age должен быть числом, получили ${typeof value}`);
      }
      // Reflect.set корректно обрабатывает receiver и инварианты
      return Reflect.set(t, prop, value, receiver);
    },
  });
}

const user = createValidated<User>({ name: 'Alice', age: 25 });
user.age = 30;          // OK
// user.age = '30';    // TypeError: age должен быть числом

Пример: Реактивный объект (упрощённый Vue 3)

type EffectFn = () => void;

let activeEffect: EffectFn | null = null;
const deps = new WeakMap<object, Map<string | symbol, Set<EffectFn>>>();

function track(target: object, key: string | symbol) {
  if (!activeEffect) return;
  if (!deps.has(target)) deps.set(target, new Map());
  const map = deps.get(target)!;
  if (!map.has(key)) map.set(key, new Set());
  map.get(key)!.add(activeEffect);
}

function trigger(target: object, key: string | symbol) {
  deps.get(target)?.get(key)?.forEach(fn => fn());
}

function reactive<T extends object>(raw: T): T {
  return new Proxy(raw, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver); // receiver сохраняет this
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return result; // обязательно возвращаем boolean
    },
  });
}

const state = reactive({ count: 0 });

function watchEffect(fn: EffectFn) {
  activeEffect = fn;
  fn(); // первый запуск — собирает зависимости
  activeEffect = null;
}

watchEffect(() => console.log('count:', state.count)); // count: 0
state.count++;  // автоматически: count: 1

Пример: Reflect — разница с Object

const obj = { _x: 0, get x() { return this._x; } };
const child = Object.create(obj);

// Наивная ловушка — теряет receiver, this === obj
const bad = new Proxy(child, {
  get(target, key) {
    return target[key as keyof typeof target]; // this в геттере === obj
  },
});

// Правильная ловушка — receiver === bad, this в геттере === bad
const good = new Proxy(child, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});

console.log(bad.x);  // читает obj._x, а не child._x
console.log(good.x); // корректно читает child._x через прототипный геттер

Типичные ошибки

Игнорирование receiver в ловушках get/set — это ломает наследование и геттеры в прототипе

Возврат из ловушки set значения, отличного от true при успехе — в strict mode это TypeError

Путаница между Reflect и Object — кандидаты не знают, чем они отличаются и почему Reflect появился отдельно

Попытка проксировать встроенные объекты (Map, Set, Date) без пробрасывания this через bind — внутренние [[Slots]] недоступны через Proxy

Создание Proxy поверх Proxy без понимания накладных расходов — каждая ловушка вызывается синхронно и может стать узким местом

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.5
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее