Proxy Pattern (GoF) — Прокси

Перехват доступа к объекту. В JS — встроенный new Proxy(target, handler). Используется для метапрограммирования, ленивых вычислений, валидации, логирования.

Проблема

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

Где используется

  • Ленивая инициализация тяжёлых объектов (Virtual Proxy)
  • Контроль доступа и аутентификация (Protection Proxy)
  • Кэширование результатов (Caching Proxy)
  • Реактивность: Vue 3 использует Proxy для отслеживания изменений
  • Логирование и профилирование API
  • Транзакционные объекты, dirty-tracking в ORM

Решение

  • Встроенный Proxy принимает target и handler с ловушками: get, set, has, deleteProperty, apply, construct и др.
  • Прокси — отдельный объект, proxy !== target.
  • Без handler'а — прозрачный прокси.

Реализации

Базовый Proxy

const data = { name: 'Marcus' };

const person = new Proxy(data, {
  get(target, key) {
    console.log('read', key);
    return key in target ? target[key] : 0; // дефолт 0
  },
  set(target, key, value) {
    if (typeof value !== typeof target[key]) throw new TypeError('type mismatch');
    target[key] = value;
    return true;
  },
});

person.name;        // 'read name' → 'Marcus'
person.age;         // 'read age'  → 0
person.name = 42;   // TypeError

Virtual Proxy (ленивая загрузка)

class RealImageLoader {
  constructor(filename) {
    this.filename = filename;
    console.log(`Загрузка: ${filename}`); // дорогая операция
  }
  display { console.log(`Отображение: ${this.filename}`); }
}

class ImageProxy {
  constructor(filename) {
    this.filename = filename;
    this._real = null;
  }
  display {
    if (!this._real) this._real = new RealImageLoader(this.filename);
    this._real.display;
  }
}

const image = new ImageProxy('photo.jpg'); // загрузки нет
image.display; // только сейчас: 'Загрузка...'
image.display; // 'Отображение' (уже загружено)

Revocable Proxy

const { proxy, revoke } = Proxy.revocable(data, {});
revoke;
proxy.name; // TypeError: revoked

Реактивность через Proxy

function reactive(target, onChange) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const old = obj[prop];
      obj[prop] = value;
      if (old !== value) onChange(prop, old, value);
      return true;
    }
  });
}

const state = reactive({ count: 0 }, (prop, old, next) => {
  console.log(`${prop}: ${old}${next}`);
});

state.count++;  // 'count: 0 → 1'

Валидация при записи

const validated = new Proxy({}, {
  set(target, prop, value) {
    if (typeof value !== 'number') {
      throw new TypeError(`${prop} должно быть числом`);
    }
    target[prop] = value;
    return true;
  }
});

Где используется в JS-экосистеме

  • Vue 3 reactivityProxy вместо Object.defineProperty
  • MobX 5+ — observable через Proxy
  • immer.js — иммутабельные обновления через Proxy
  • node-test mocks — ленивая инициализация моков
  • ORM dirty-tracking — детект изменений в моделях
  • Логирование/трассировка — невидимый прокси с логированием

Подводные камни

  • Proxy vs Decorator: Decorator расширяет поведение (метаданные); Proxy перехватывает доступ (контроль, защита).
  • Proxy vs Flyweight: оба «за абстракцией ещё абстракция»; Flyweight — экономия памяти, Proxy — перехват.
  • proxy !== target — может ломать ===-сравнения и identity checks.
  • Производительность: каждое обращение через handler — это вызов функции, дорого в hot path.
  • Не все объекты прокси-фрэндли: Date, Map, Set имеют internal slots, ломаются без Reflect.
  • Не возвращать true из set-ловушки — в strict mode это вызывает TypeError.
  • Перехват this — внутри методов нужно использовать Reflect.get(target, prop, receiver).
  • Circular proxy — прокси над прокси может создать бесконечную рекурсию.

Главные тезисы автора

  • «Proxy — это паттерн из ООП. Боксинг значений и перехват всех обращений».
  • «В JS нельзя перехватить конструктор/вызов функции на классах. Встроенный Proxy позволяет это делать».
  • Перехват по имени, которое заранее не знаем — главное преимущество JS-Proxy.
  • «Похож на фасад» — кладём объект внутрь, наружу делегируем интерфейс.
  • Transactional objects на Proxy — собирать изменения, откатывать или коммитить.
  • Revocable proxy — отменяемое связывание для безопасной передачи.

🎓 Источники

См. также