Kodokon kodokon.com

Proxy, Reflect и метапрограммирование

Перехватывай фундаментальные операции языка через ловушки Proxy и аккуратно делегируй поведение по умолчанию через Reflect.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Proxy оборачивает целевой объект и перехватывает внутренние операции языка через тринадцать ловушек: get, set, has (оператор in), deleteProperty, ownKeys, apply, construct и так далее. Объект Reflect предоставляет ровно те же операции в виде функций, с теми же сигнатурами, что и у ловушек. Это фундамент реактивности во Vue 3, динамических моков и API в стиле ORM.

JAVASCRIPT
const target = { name: "Ada" };

const traced = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log("get " + String(prop));
    return Reflect.get(obj, prop, receiver);
  }
});

traced.name; // get name
Минимальная ловушка get, делегированная в Reflect.

Почему Reflect.get(obj, prop, receiver), а не obj[prop]? Из-за получателя (receiver). Если свойство - это аксессор (геттер), Reflect.get вызовет его с this, указывающим на получателя - на прокси или на объект, наследующий от него, - тогда как obj[prop] установил бы this на сырую цель. Без получателя геттеры, которые читают this, ломаются, как только какой-нибудь объект наследует от прокси.

JAVASCRIPT
const base = {
  get label() {
    return this.name;
  }
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

const child = Object.create(proxy);
child.name = "Ada";
console.log(child.label); // "Ada" thanks to the receiver
Получатель сохраняет this в унаследованных геттерах.

Серьёзные сценарии применения: валидация при записи, виртуальные свойства, вычисляемые на лету, логирование, контроль доступа (скрытие ключей с префиксом _ через has и ownKeys), объекты с поведением по умолчанию. Учти, что Reflect.set возвращает булево значение вместо того, чтобы бросать исключение в строгом режиме: твоя ловушка обязана пробросить это возвращаемое значение.

JAVASCRIPT
function validated(target) {
  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && !Number.isInteger(value)) {
        throw new TypeError("age must be an integer");
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = validated({ age: 30 });
user.age = 31;       // ok
// user.age = "old"; // TypeError
Прозрачная валидация при записи.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Почему в ловушке get стоит предпочесть Reflect.get(target, prop, receiver), а не target[prop]?
    • Так быстрее, потому что Reflect обходит прототип
    • Чтобы геттеры выполнялись с правильным this (получателем), а не с сырой целью
    • Потому что target[prop] заново вызвал бы ловушку и привёл к бесконечной рекурсии
    • Обе формы строго эквивалентны
  2. Какая ловушка перехватывает выражение "id" in obj?
    • get
    • has
    • ownKeys
    • getOwnPropertyDescriptor
  3. Ловушка get возвращает 42 для свойства цели, значение которого равно 1 и которое объявлено неконфигурируемым и незаписываемым. Что произойдёт?
    • Прокси вернёт 42, ловушка всегда имеет приоритет
    • Движок молча вернёт 1
    • Движок бросит TypeError из-за нарушения инварианта
    • Поведение зависит от движка JavaScript