Объект 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.

  1. Операция записи proxy.test= устанавливает значение target.
  2. Операция чтения proxy.test возвращает значение из target.
  3. Итерация по 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. lengthproperty автоматически увеличивается при добавлении значений. Наш прокси ничего не ломает.

Нам не нужно переопределять методы массива, добавляющие значение, такие как push и unshift и т. д., чтобы добавить туда проверки, потому что внутри они используют операцию [[Set]], которая перехватывается прокси.

Могут быть более практические варианты использования, которые мы можем применить с прокси, например, определение частных свойств в итерации объекта с помощью «ownKeys» и «getOwnPropertyDescriptor».

Для каждого внутреннего метода в этой таблице есть ловушка: имя метода, которое мы можем добавить к параметру handler new Proxy для перехвата операции: