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'
Спецификации
Совместимость с браузерами
| Настольные компьютеры | Мобильные | Сервер | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 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