Объект Proxy
оборачивает другой объект и перехватывает операции, такие как чтение/запись свойств и другие, при необходимости обрабатывая их самостоятельно или прозрачно позволяя объекту обрабатывать их.
Прокси используются во многих библиотеках и некоторых браузерных фреймворках. Мы увидим много
Синтаксис
let proxy = new Proxy(target, handler)
target
— объект для обертывания, может быть чем угодно, в том числе и функциями.handler
— конфигурация прокси: объект с «ловушками», методами, перехватывающими операции. – напр.get
ловушка для чтения свойстваtarget
,set
ловушка для записи свойства вtarget
и так далее.
Для операций на proxy
, если есть соответствующая ловушка в handler
, то она срабатывает, и у прокси есть шанс ее обработать, иначе операция выполняется на target
.
Для начала создадим прокси без ловушек:
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 5; // writing to proxy (1)
console.log(target.test); // 5, the property appeared in target!
console.log(proxy.test); // 5, we can read it from proxy too (2)
for(let key in proxy) console.log(key); // test, iteration works (3)
Поскольку ловушек нет, все операции на proxy
перенаправляются на target
.
- Операция записи
proxy.test=
устанавливает значениеtarget
. - Операция чтения
proxy.test
возвращает значение изtarget
. - Итерация по
proxy
возвращает значения изtarget
.
Как мы видим, без всяких ловушек proxy
является прозрачной оболочкой вокруг target
.
Proxy
— особый «экзотический объект». У него нет собственных свойств. С пустым handler
он прозрачно перенаправляет операции на target
.
Чтобы активировать больше возможностей, давайте добавим ловушки.
Что мы можем перехватить с их помощью?
Для большинства операций с объектами в спецификации JavaScript существует так называемый «внутренний метод», описывающий, как он работает на самом низком уровне. Например, [[Get]]
— внутренний метод для чтения свойства, [[Set]]
— внутренний метод для записи свойства и так далее. Эти методы используются только в спецификации, мы не можем вызывать их напрямую по имени.
Прокси-ловушки перехватывают вызовы этих методов. Они перечислены в Спецификации прокси и в таблице ниже.
Давайте посмотрим, как это работает на практических примерах.
Значение по умолчанию с ловушкой «получить»
Наиболее распространенные ловушки связаны со свойствами чтения/записи.
Для перехвата чтения у handler
должен быть метод get(target, property, receiver)
.
Он срабатывает при чтении свойства со следующими аргументами:
target
— целевой объект, который передается в качестве первого аргументаnew Proxy
,property
– имя свойства,receiver
— если целевое свойство является геттером, тоreceiver
— это объект, который будет использоваться какthis
в его вызове. Обычно это сам объектproxy
(или объект, который наследуется от него, если мы наследуем от прокси). Сейчас нам не нужен этот аргумент, поэтому он будет объяснен более подробно позже.
Давайте используем get
для реализации значений по умолчанию для объекта.
Мы создадим числовой массив, который возвращает 0
для несуществующих значений.
Обычно, когда кто-то пытается получить несуществующий элемент массива, он получает undefined
, но мы обернем обычный массив в прокси, который перехватывает чтение и возвращает 0
, если такого свойства нет:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // default value } } });
alert( numbers[1] ); // 1 alert( numbers[123] ); // 0 (no such item)
Проверка с ловушкой «Set»
Допустим, нам нужен массив исключительно для чисел. Если добавляется значение другого типа, должна быть ошибка.
Ловушка set
срабатывает при записи свойства.
set(target, property, value, receiver)
:
target
— целевой объект, который передается в качестве первого аргументаnew Proxy
,property
– имя свойства,value
– значение свойства,receiver
— аналогично ловушкеget
, имеет значение только для свойств сеттера.
Ловушка set
должна возвращать true
в случае успешной установки и false
в противном случае (срабатывает TypeError
).
Давайте используем его для проверки новых значений:
let numbers = [];
numbers = new Proxy(numbers, { // (*) set(target, prop, val) { // to intercept property writing if (typeof val == 'number') { target[prop] = val; return true; } else { return false; } } });
numbers.push(1); // added successfully numbers.push(2); // added successfully console.log("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
console.log("This line is never reached (error in the line above)");
Обратите внимание: встроенный функционал массивов все еще работает! Значения добавляются push
. length
property автоматически увеличивается при добавлении значений. Наш прокси ничего не ломает.
Нам не нужно переопределять методы массива, добавляющие значение, такие как push
и unshift
и т. д., чтобы добавить туда проверки, потому что внутри они используют операцию [[Set]]
, которая перехватывается прокси.
Могут быть более практические варианты использования, которые мы можем применить с прокси, например, определение частных свойств в итерации объекта с помощью «ownKeys» и «getOwnPropertyDescriptor».
Для каждого внутреннего метода в этой таблице есть ловушка: имя метода, которое мы можем добавить к параметру handler
new Proxy
для перехвата операции: