Spec-Zone.ru › JavaScript

Proxy

Baseline Широко доступно

Эта функция хорошо зарекомендовала себя и работает на многих устройствах и в различных версиях браузеров. Она доступна во всех браузерах с сентября 2016 года.

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

Описание

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

Вы создаете Proxy с двумя параметрами:

  • target: исходный объект, для которого вы хотите создать прокси
  • handler: объект, который определяет, какие операции будут перехвачены и как переопределить перехваченные операции.

Например, этот код создает прокси для объекта target.

const target = {
  message1: "hello",
  message2: "everyone",
};

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

Поскольку обработчик пуст, этот прокси ведет себя так же, как и исходная цель:

console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone

Чтобы настроить прокси, мы определяем функции в объекте обработчика:

const target = {
  message1: "hello",
  message2: "everyone",
};

const handler2 = {
  get(target, prop, receiver) {
    return "world";
  },
};

const proxy2 = new Proxy(target, handler2);

Здесь мы предоставили реализацию обработчика get(), который перехватывает попытки доступа к свойствам цели.

Функции обработчика иногда называются ловушками (traps), предположительно потому, что они перехватывают вызовы к целевому объекту. Ловушка в handler2 выше переопределяет все аксессоры свойств:

console.log(proxy2.message1); // world
console.log(proxy2.message2); // world

Прокси часто используются с объектом Reflect, который предоставляет некоторые методы с теми же именами, что и ловушки Proxy. Методы Reflect предоставляют рефлексивную семантику для вызова соответствующих внутренних методов объекта. Например, мы можем вызвать Reflect.get, если не хотим переопределять поведение объекта:

const target = {
  message1: "hello",
  message2: "everyone",
};

const handler3 = {
  get(target, prop, receiver) {
    if (prop === "message2") {
      return "world";
    }
    return Reflect.get(...arguments);
  },
};

const proxy3 = new Proxy(target, handler3);

console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world

Метод Reflect по-прежнему взаимодействует с объектом через внутренние методы объекта — он не "де-проксирует" прокси, если вызывается для прокси. Если вы используете методы Reflect в ловушке прокси, и вызов метода Reflect снова перехватывается ловушкой, может возникнуть бесконечная рекурсия.

Терминология

Следующие термины используются при обсуждении функциональности прокси.

обработчик
Объект, передаваемый вторым аргументом конструктору Proxy. Он содержит ловушки, которые определяют поведение прокси.
ловушка
Функция, определяющая поведение соответствующего внутреннего метода объекта. (Это аналогично концепции ловушек в операционных системах.)
цель
Объект, который виртуализирует прокси. Часто используется как хранилище для прокси. Инварианты (семантика, которая остается неизменной) относительно недопустимости расширения объекта или неотчуждаемых свойств проверяются по цели.
инварианты
Семантика, которая остается неизменной при реализации пользовательских операций. Если ваша реализация ловушки нарушает инварианты обработчика, будет выброшено TypeError.

Внутренние методы объекта

Объекты — это коллекции свойств. Однако язык не предоставляет никаких средств для прямого манипулирования данными, хранящимися в объекте — скорее, объект определяет некоторые внутренние методы, указывающие, как с ним можно взаимодействовать. Например, когда вы читаете obj.x, вы можете ожидать, что произойдет следующее:

  • Свойство x ищется вверх по цепочке прототипов, пока не будет найдено.
  • Если x является свойством данных, возвращается value дескриптора свойства.
  • Если x является свойством-аксессором, вызывается геттер, и возвращается возвращаемое значение геттера.

В этом процессе нет ничего особенного в языке — это просто потому, что обычные объекты по умолчанию имеют внутренний метод [[Get]], определенный с таким поведением. Синтаксис доступа к свойству obj.x просто вызывает метод [[Get]] для объекта, и объект использует собственную реализацию внутреннего метода, чтобы определить, что возвращать.

В качестве другого примера массивы отличаются от обычных объектов, потому что у них есть магическое свойство length, которое при изменении автоматически выделяет пустые слоты или удаляет элементы из массива. Аналогично, добавление элементов массива автоматически изменяет свойство length. Это связано с тем, что массивы имеют внутренний метод [[DefineOwnProperty]], который знает, как обновлять length при записи по целочисленному индексу, или обновлять содержимое массива при записи в length. Такие объекты, внутренние методы которых имеют отличные от обычных объектов реализации, называются экзотическими объектами. Proxy позволяют разработчикам определять собственные экзотические объекты с полным набором возможностей.

Все объекты имеют следующие внутренние методы:

Внутренний метод Соответствующая ловушка
[[GetPrototypeOf]] getPrototypeOf()
[[SetPrototypeOf]] setPrototypeOf()
[[IsExtensible]] isExtensible()
[[PreventExtensions]] preventExtensions()
[[GetOwnProperty]] getOwnPropertyDescriptor()
[[DefineOwnProperty]] defineProperty()
[[HasProperty]] has()
[[Get]] get()
[[Set]] set()
[[Delete]] deleteProperty()
[[OwnPropertyKeys]] ownKeys()

Функциональные объекты также имеют следующие внутренние методы:

Внутренний метод Соответствующая ловушка
[[Call]] apply()
[[Construct]] construct()

Важно понимать, что все взаимодействия с объектом в конечном итоге сводятся к вызову одного из этих внутренних методов, и все они настраиваются через прокси. Это означает, что почти ни одно поведение (за исключением некоторых критических инвариантов) не гарантируется в языке — всё определяется самим объектом. Когда вы выполняете delete obj.x, нет гарантии, что "x" in obj вернет false после этого — это зависит от реализаций объекта [[Delete]] и [[HasProperty]]. delete obj.x может записывать что-то в консоль, изменять глобальное состояние или даже определять новое свойство вместо удаления существующего, хотя таких семантик следует избегать в собственном коде.

Все внутренние методы вызываются самим языком и напрямую недоступны в коде JavaScript. Пространство имен Reflect предлагает методы, которые делают не более чем вызывают внутренние методы, помимо некоторой нормализации/валидации входных данных. На странице каждой ловушки мы перечисляем несколько типичных ситуаций, когда ловушка вызывается, но эти внутренние методы вызываются в многих местах. Например, методы массива читают и записывают в массив через эти внутренние методы, поэтому методы, такие как push(), также будут вызывать ловушки get() и set().

Большинство внутренних методов выполняют свои задачи прямолинейно. Единственные два, которые могут вызывать путаницу, — это [[Set]] и [[DefineOwnProperty]]. Для обычных объектов первый вызывает сеттеры; второй — нет. (И [[Set]] внутренне вызывает [[DefineOwnProperty]], если существующего свойства нет или свойство является свойством данных.) Хотя вы можете знать, что синтаксис obj.x = 1 использует [[Set]], а Object.defineProperty() использует [[DefineOwnProperty]], не сразу очевидно, какую семантику используют другие встроенные методы и синтаксисы. Например, поля классов используют семантику [[DefineOwnProperty]], поэтому сеттеры, определенные в суперклассе, не вызываются при объявлении поля в производном классе.

Конструктор

Proxy()
Создает новый объект Proxy.

Примечание: Нет свойства Proxy.prototype, поэтому экземпляры Proxy не имеют специальных свойств или методов.

Статические методы

Proxy.revocable()
Создает отзывной объект Proxy.

Примеры

Основной пример

В этом примере число 37 возвращается как значение по умолчанию, когда имя свойства отсутствует в объекте. Используется обработчик get().

const handler = {
  get(obj, prop) {
    return prop in obj ? obj[prop] : 37;
  },
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined

console.log("c" in p, p.c); // false, 37

Прокси-переадресация без операций

В этом примере мы используем нативный объект JavaScript, которому наш прокси будет перенаправлять все операции, применяемые к нему.

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

p.a = 37; // Operation forwarded to the target

console.log(target.a); // 37 (The operation has been properly forwarded!)

Обратите внимание, что хотя эта "без-операционная" переадресация работает для обычных объектов JavaScript, она не работает для нативных объектов, таких как DOM-элементы, объекты Map или чего-либо, имеющего внутренние слоты. См. отсутствие переадресации приватных полей для получения дополнительной информации.

Отсутствие переадресации приватных полей

Прокси — это все еще другой объект с другой идентичностью — это прокси, действующий между обернутым объектом и внешним миром. Как таковой, прокси не имеет прямого доступа к приватным элементам исходного объекта.

class Secret {
  #secret;
  constructor(secret) {
    this.#secret = secret;
  }
  get secret() {
    return this.#secret.replace(/\d+/, "[REDACTED]");
  }
}

const secret = new Secret("123456");
console.log(secret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(secret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it

Это связано с тем, что при вызове ловушки get прокси значение this является proxy вместо исходного secret, поэтому #secret недоступен. Чтобы исправить это, используйте исходный secret как this:

const proxy = new Proxy(secret, {
  get(target, prop, receiver) {
    // By default, it looks like Reflect.get(target, prop, receiver)
    // which has a different value of `this`
    return target[prop];
  },
});
console.log(proxy.secret);

Для методов это означает, что вам также придется перенаправить значение this метода на исходный объект:

class Secret {
  #x = 1;
  x() {
    return this.#x;
  }
}

const secret = new Secret();
const proxy = new Proxy(secret, {
  get(target, prop, receiver) {
    const value = target[prop];
    if (value instanceof Function) {
      return function (...args) {
        return value.apply(this === receiver ? target : this, args);
      };
    }
    return value;
  },
});
console.log(proxy.x());

Некоторые нативные объекты JavaScript имеют свойства, называемые внутренними слотами, которые недоступны из кода JavaScript. Например, объекты Map имеют внутренний слот, называемый [[MapData]], который хранит пары ключ-значение карты. Таким образом, вы не можете легко создать прокси-переадресацию для карты:

const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy

Чтобы обойти это, вам нужно использовать "восстанавливающий" прокси this, показанный выше.

Валидация

С помощью Proxy вы можете легко проверять переданное значение для объекта. Этот пример использует обработчик set().

const validator = {
  set(obj, prop, value) {
    if (prop === "age") {
      if (!Number.isInteger(value)) {
        throw new TypeError("The age is not an integer");
      }
      if (value > 200) {
        throw new RangeError("The age seems invalid");
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  },
};

const person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = "young"; // Throws an exception
person.age = 300; // Throws an exception

Манипулирование DOM-узлами

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

Мы создаем объект view, который является прокси для объекта со свойством selected. Обработчик прокси определяет обработчик set().

Когда мы присваиваем HTML-элемент view.selected, атрибут 'aria-selected' этого элемента устанавливается на true. Если мы затем присвоим другой элемент view.selected, атрибут 'aria-selected' этого элемента устанавливается на true, а атрибут 'aria-selected' предыдущего элемента автоматически устанавливается на false.

const view = new Proxy(
  {
    selected: null,
  },
  {
    set(obj, prop, newVal) {
      const oldVal = obj[prop];

      if (prop === "selected") {
        if (oldVal) {
          oldVal.setAttribute("aria-selected", "false");
        }
        if (newVal) {
          newVal.setAttribute("aria-selected", "true");
        }
      }

      // The default behavior to store the value
      obj[prop] = newVal;

      // Indicate success
      return true;
    },
  },
);

const item1 = document.getElementById("item-1");
const item2 = document.getElementById("item-2");

// select item1:
view.selected = item1;

console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: true

// selecting item2 de-selects item1:
view.selected = item2;

console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: false

console.log(`item2: ${item2.getAttribute("aria-selected")}`);
// item2: true

Коррекция значения и дополнительное свойство

Прокси-объект products оценивает переданное значение и при необходимости преобразует его в массив. Объект также поддерживает дополнительное свойство latestBrowser как геттер, так и сеттер.

const products = new Proxy(
  {
    browsers: ["Firefox", "Chrome"],
  },
  {
    get(obj, prop) {
      // An extra property
      if (prop === "latestBrowser") {
        return obj.browsers[obj.browsers.length - 1];
      }

      // The default behavior to return the value
      return obj[prop];
    },
    set(obj, prop, value) {
      // An extra property
      if (prop === "latestBrowser") {
        obj.browsers.push(value);
        return true;
      }

      // Convert the value if it is not an array
      if (typeof value === "string") {
        value = [value];
      }

      // The default behavior to store the value
      obj[prop] = value;

      // Indicate success
      return true;
    },
  },
);

console.log(products.browsers);
//  ['Firefox', 'Chrome']

products.browsers = "Safari";
//  pass a string (by mistake)

console.log(products.browsers);
//  ['Safari'] <- no problem, the value is an array

products.latestBrowser = "Edge";

console.log(products.browsers);
//  ['Safari', 'Edge']

console.log(products.latestBrowser);
//  'Edge'

Спецификации

Спецификация
ECMAScript® 2027 Language Specification
# sec-proxy-objects

Совместимость с браузерами

Настольные компьютеры Мобильные Сервер
Chrome Edge Firefox Opera Safari Chrome Android Firefox for Android Opera Android Safari on iOS Samsung Internet WebView Android WebView on iOS Bun Deno Node.js
Proxy
49
12
18
36
10
49
18
36
10
5.0
49
10
1.0.0
1.0
6.0.0
Proxy
49
12
18
36
10
49
18
36
10
5.0
49
10
1.0.0
1.0
6.0.0
revocable
63
12
34
50
10
63
34
46
10
8.0
63
10
1.0.0
1.0
6.0.0

См. также

  • Proxies are awesome презентация Брендана Айка на JSConf (2014)

© 2005–2025 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API