JavaScript Proxy и Reflect

16 июня 2026
Автор

Антон Ларичев

Что такое Proxy

Proxy — это обёртка вокруг объекта, которая позволяет перехватывать и переопределять фундаментальные операции: чтение свойств, запись, удаление, проверку наличия и многое другое. Вместо того чтобы работать с объектом напрямую, вы работаете с его прокси, который может изменять поведение на лету.

Концепция проста: есть целевой объект (target) и обработчик (handler) с набором ловушек (traps). Каждая ловушка — это метод, который вызывается вместо стандартной операции.

const target = { name: 'Alice', age: 30 };

const handler = {
  get(target, property) {
    console.log(`Читаем свойство: ${property}`);
    return target[property];
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Читаем свойство: name → Alice
console.log(proxy.age);  // Читаем свойство: age → 30

Синтаксис и базовая структура

Конструктор Proxy принимает два аргумента:

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

Если handler пустой, прокси просто пропускает все операции к целевому объекту без изменений:

const obj = { value: 42 };
const proxy = new Proxy(obj, {});

console.log(proxy.value); // 42 — прозрачный прокси

Основные ловушки (traps)

get — перехват чтения свойств

Ловушка get срабатывает при обращении к свойству объекта.

const defaults = new Proxy({}, {
  get(target, property) {
    return property in target ? target[property] : `Свойство "${property}" не найдено`;
  }
});

defaults.username = 'Bob';
console.log(defaults.username); // Bob
console.log(defaults.email);    // Свойство "email" не найдено

Практический пример — ленивая загрузка данных:

function createLazyLoader(fetchFn) {
  const cache = {};

  return new Proxy({}, {
    get(target, property) {
      if (!(property in cache)) {
        cache[property] = fetchFn(property);
      }
      return cache[property];
    }
  });
}

const api = createLazyLoader((key) => `Fetched: ${key}`);
console.log(api.users);    // Fetched: users
console.log(api.users);    // Fetched: users (из кэша)
console.log(api.products); // Fetched: products

set — перехват записи свойств

Ловушка set позволяет контролировать, какие значения можно присваивать свойствам. Метод должен возвращать true при успехе, иначе будет выброшен TypeError.

const validator = new Proxy({}, {
  set(target, property, value) {
    if (property === 'age') {
      if (typeof value !== 'number') {
        throw new TypeError('Возраст должен быть числом');
      }
      if (value < 0 || value > 150) {
        throw new RangeError('Возраст должен быть от 0 до 150');
      }
    }
    target[property] = value;
    return true;
  }
});

validator.name = 'Charlie'; // OK
validator.age = 25;         // OK
validator.age = -5;         // RangeError: Возраст должен быть от 0 до 150
validator.age = 'old';      // TypeError: Возраст должен быть числом

has — перехват оператора in

Ловушка has срабатывает при проверке property in proxy.

const range = new Proxy({ min: 1, max: 100 }, {
  has(target, property) {
    const num = Number(property);
    return num >= target.min && num <= target.max;
  }
});

console.log(50 in range);  // true
console.log(0 in range);   // false
console.log(101 in range); // false

deleteProperty — перехват удаления

const protected = new Proxy({ id: 1, name: 'Admin', temp: 'data' }, {
  deleteProperty(target, property) {
    if (property === 'id' || property === 'name') {
      throw new Error(`Нельзя удалить защищённое свойство: ${property}`);
    }
    delete target[property];
    return true;
  }
});

delete protected.temp;  // OK
delete protected.id;    // Error: Нельзя удалить защищённое свойство: id

apply — перехват вызова функции

Ловушка apply работает, если target является функцией:

function sum(a, b) {
  return a + b;
}

const loggedSum = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log(`Вызов с аргументами: ${args}`);
    const result = target.apply(thisArg, args);
    console.log(`Результат: ${result}`);
    return result;
  }
});

loggedSum(3, 4);
// Вызов с аргументами: 3,4
// Результат: 7

construct — перехват оператора new

function User(name, role) {
  this.name = name;
  this.role = role;
}

const SafeUser = new Proxy(User, {
  construct(target, args) {
    const [name, role] = args;
    if (!['admin', 'user', 'guest'].includes(role)) {
      throw new Error(`Недопустимая роль: ${role}`);
    }
    return new target(name, role);
  }
});

const u1 = new SafeUser('Alice', 'admin'); // OK
const u2 = new SafeUser('Bob', 'superuser'); // Error: Недопустимая роль: superuser

Reflect API

Reflect — это встроенный объект, методы которого соответствуют каждой ловушке Proxy. Он предоставляет стандартные реализации операций, которые можно вызывать внутри ловушек.

Зачем нужен Reflect

Внутри ловушки Proxy нельзя просто обратиться к target[property] в сложных сценариях — это может нарушить инварианты прокси или привести к рекурсии. Reflect даёт правильный способ выполнить операцию «по умолчанию»:

const handler = {
  get(target, property, receiver) {
    console.log(`get: ${property}`);
    // Reflect.get корректно обрабатывает prototype chain и receiver
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`set: ${property} = ${value}`);
    return Reflect.set(target, property, value, receiver);
  }
};

const proxy = new Proxy({ x: 1 }, handler);
proxy.x;     // get: x
proxy.y = 2; // set: y = 2

Методы Reflect

Каждый метод Reflect соответствует одной ловушке:

const obj = { a: 1 };

Reflect.get(obj, 'a');          // 1 — аналог obj.a
Reflect.set(obj, 'b', 2);       // true — аналог obj.b = 2
Reflect.has(obj, 'a');          // true — аналог 'a' in obj
Reflect.deleteProperty(obj, 'a'); // true — аналог delete obj.a
Reflect.ownKeys(obj);           // ['b'] — аналог Object.getOwnPropertyNames + Symbols

function greet(name) { return `Hello, ${name}`; }
Reflect.apply(greet, null, ['World']); // Hello, World

Reflect vs прямые операции

Главное преимущество Reflect — возврат булевых значений вместо исключений для операций, которые могут провалиться:

// Прямое удаление бросает исключение при строгом режиме
// если свойство не configurable
try {
  delete Object.freeze({}).prop; // В strict режиме — TypeError
} catch (e) {
  console.log(e.message);
}

// Reflect возвращает false без исключения
const frozen = Object.freeze({ prop: 1 });
console.log(Reflect.deleteProperty(frozen, 'prop')); // false — спокойно

Практические применения

Логирование и аудит

function createAuditProxy(target, logger = console.log) {
  return new Proxy(target, {
    get(t, prop, receiver) {
      logger(`[READ] ${String(prop)}`);
      return Reflect.get(t, prop, receiver);
    },
    set(t, prop, value, receiver) {
      logger(`[WRITE] ${String(prop)} = ${JSON.stringify(value)}`);
      return Reflect.set(t, prop, value, receiver);
    },
    deleteProperty(t, prop) {
      logger(`[DELETE] ${String(prop)}`);
      return Reflect.deleteProperty(t, prop);
    }
  });
}

const config = createAuditProxy({ debug: false, version: '1.0' });
config.debug;        // [READ] debug
config.debug = true; // [WRITE] debug = true
delete config.debug; // [DELETE] debug

Реактивная система данных

Прокси лежат в основе реактивности во Vue 3:

function reactive(target, onChange) {
  return new Proxy(target, {
    set(t, prop, value, receiver) {
      const oldValue = t[prop];
      const result = Reflect.set(t, prop, value, receiver);
      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }
      return result;
    }
  });
}

const state = reactive(
  { count: 0, name: 'App' },
  (prop, newVal, oldVal) => {
    console.log(`${prop}: ${oldVal} → ${newVal}`);
    // Здесь можно перерисовать UI
  }
);

state.count = 1; // count: 0 → 1
state.count = 1; // Нет вывода — значение не изменилось
state.name = 'MyApp'; // name: App → MyApp

Валидация схемы объекта

function createTypedObject(schema) {
  return new Proxy({}, {
    set(target, property, value) {
      if (!(property in schema)) {
        throw new Error(`Неизвестное свойство: ${property}`);
      }
      const expectedType = schema[property];
      if (typeof value !== expectedType) {
        throw new TypeError(
          `${property} должно быть ${expectedType}, получено ${typeof value}`
        );
      }
      return Reflect.set(target, property, value);
    }
  });
}

const user = createTypedObject({
  name: 'string',
  age: 'number',
  active: 'boolean'
});

user.name = 'Diana';  // OK
user.age = 28;        // OK
user.active = true;   // OK
user.age = '28';      // TypeError: age должно быть number, получено string
user.email = 'test';  // Error: Неизвестное свойство: email

Отрицательные индексы для массивов

function createArrayWithNegativeIndex(arr) {
  return new Proxy(arr, {
    get(target, property, receiver) {
      const index = Number(property);
      if (Number.isInteger(index) && index < 0) {
        return Reflect.get(target, target.length + index, receiver);
      }
      return Reflect.get(target, property, receiver);
    }
  });
}

const arr = createArrayWithNegativeIndex([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
console.log(arr[0]);  // 1

Ограничения и подводные камни

Прокси не прозрачен для идентичности

const target = {};
const proxy = new Proxy(target, {});

console.log(proxy === target); // false
console.log(proxy instanceof Object); // true

Производительность

Каждый перехват добавляет накладные расходы. Если прокси используется в критически важных циклах, это может заметно замедлить работу. Применяйте прокси там, где гибкость важнее скорости.

Встроенные объекты требуют особого внимания

Некоторые встроенные типы (Map, Set, WeakMap) хранят внутреннее состояние в слотах, недоступных через прокси. Для корректной работы нужно привязывать контекст:

const map = new Map();
const proxy = new Proxy(map, {
  get(target, property, receiver) {
    const value = Reflect.get(target, property, receiver);
    // Методы Map требуют оригинальный this
    return typeof value === 'function' ? value.bind(target) : value;
  }
});

proxy.set('key', 'value'); // Работает корректно
console.log(proxy.get('key')); // value

Отмена прокси

Proxy.revocable создаёт прокси, который можно отключить:

const { proxy, revoke } = Proxy.revocable({ data: 'secret' }, {});

console.log(proxy.data); // secret

revoke();

try {
  console.log(proxy.data); // TypeError: Cannot perform 'get' on a revoked proxy
} catch (e) {
  console.log(e.message);
}

Это полезно для временного доступа к данным — после отзыва прокси становится полностью недоступным.

Итог

Proxy и Reflect — это мощные метапрограммные инструменты JavaScript, которые открывают возможности для:

  • валидации и контроля данных на уровне объекта
  • логирования и аудита без изменения исходного кода
  • реактивных систем (как это делает Vue 3)
  • создания DSL и умных коллекций
  • управления доступом и безопасности

Reflect дополняет Proxy, предоставляя стандартные реализации операций, которые корректно работают с цепочками прототипов и receiver-объектами — его стоит использовать внутри ловушек вместо прямых обращений к свойствам target.

Для глубокого изучения JavaScript и работы с объектами, прокси и другими продвинутыми концепциями языка — записывайтесь на курс JavaScript для профессионалов на PurpleSchool.

Стрелочка влевоstructuredClone — глубокое копирование объектов в JavaScriptОбъекты в JavaScriptСтрелочка вправо

Постройте личный план изучения Javascript до уровня Middle — бесплатно!

Javascript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Javascript

Как работает метод trim() - JavaScriptКак работает метод toUpperCase() - JavaScriptКак работает метод toLowerCase() - JavaScriptКак работает метод substring() - JavaScriptКак работает метод startsWith() - JavaScriptКак работает метод split() - JavaScriptКак работает метод slice() - JavaScriptКак работает метод search() - JavaScriptКак работает метод replaceAll() - JavaScriptКак работает метод replace() - JavaScriptКак работает метод repeat() - JavaScriptКак работает метод padStart() - JavaScriptКак работает метод padEnd() - JavaScriptКак работает метод matchAll() - JavaScriptКак работает метод match() - JavaScriptКак работает метод localeCompare() - JavaScriptКак работает свойство length - JavaScriptКак работает метод lastIndexOf() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод fromCodePoint() - JavaScriptКак работает метод fromCharCode() - JavaScriptКак работает метод endsWith() - JavaScriptКак работает метод concat() - JavaScriptКак работает метод charCodeAt() - JavaScriptКак работает метод charAt() - JavaScriptКак работает метод codePointAt() - JavaScript
Итератор в JavaScript
try...catch в JavaScriptError в JavaScript
Событие wheel в JavaScriptСобытие unload в JavaScriptСобытие touch в JavaScriptСобытие submit в JavaScriptСобытие scroll в JavaScriptСобытие reset в JavaScriptМетод .preventDefault() в JavaScriptСобытие mouseover в JavaScriptСобытие mouseout в JavaScriptСобытие load в JavaScriptСобытие keyup в JavaScriptСобытие keydown в JavaScriptСобытие invalid в JavaScriptСобытие input в JavaScriptСобытийная модель Event в JavaScriptОбъект события Event в JavaScriptСобытие DOMContentLoaded в JavaScriptСобытие dblclick в JavaScriptСобытие click в JavaScriptСобытие change в JavaScriptСобытие beforeunload в JavaScript
Как работает метод some() - JavaScriptКак работает метод reverse() - JavaScriptКак работает метод reduce() - JavaScriptКак работает метод map() - JavaScriptКак работает метод isArray() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод from() - JavaScriptКак работает метод forEach() - JavaScriptКак работает метод flatMap() - JavaScriptКак работает метод flat() - JavaScriptКак работает метод findIndex() - JavaScriptКак работает метод find() - JavaScriptКак работает метод filter() - JavaScriptКак работает метод every() - JavaScriptМассивы в JavaScript
Открыть базу знаний

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

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

Основы JavaScript

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

TypeScript с нуля

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

Next.js - с нуля

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

Отправить комментарий