Что такое Proxy и Reflect в JavaScript?
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-движка. Главные причины его существования:
- Дефолтное поведение в ловушках — вместо того чтобы дублировать логику движка, вызываем
Reflect.get(target, prop, receiver)и получаем поведение «как будто прокси нет». - Правильная передача
receiver— при наследованииreceiver— это реальный объект, на котором произошло обращение (а не target). ИспользованиеReflectгарантирует корректныйthisв геттерах. - Единый API —
Reflect.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 без понимания накладных расходов — каждая ловушка вызывается синхронно и может стать узким местом


