Найти в Дзене
Nuances of programming

Proxy - сокровище JavaScript

Оглавление

Источник: Nuances of Programming

Что такое прокси? Что именно он делает? Перед тем, как разобраться, посмотрим на пример из реальной жизни. У каждого из нас есть множество ежедневных задач  — просматривание электронной почты, получение экспресс-доставки и так далее. Временами мы чувствуем себя немного взволнованными: слишком много времени уходит, чтобы разобраться в огромном потоке спама, а в посылке может оказаться бомба, подброшенная террористами.

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

Теперь вернёмся к JavaScript. Мы знаем, что JS  —  объектно-ориентированный язык, то есть мы не можем написать код, не используя объекты. Но объекты в JavaScript всегда работают без оболочки, с ними можно делать что угодно. Это, в свою очередь, делает код небезопасным.

Объект Proxy представлен в ECMAScript2015. С его помощью можно найти локального администратора для объекта и расширить исходные функции объекта. На самом простом уровне применение Proxy выглядит так:

Пока это только заготовка — без обработчика этот код не работает корректно. Человек может поручить администратору чтение писем, получение доставки и тому подобные задачи. Администратор может читать и задавать свойства и так далее. Задачи могут быть расширены с помощью Proxy. В обработчике можно перечислить действия, для которых нужен Proxy. Например, чтобы отобразить инструкцию в консоли при получении свойства объекта, напишем такой код:

Обработчик из примера выше:

Когда мы читаем свойства объекта, выполняется функция get.

-2

get принимает три аргумента:

  • item  —  сам объект;
  • property  —  имя свойства, которое нужно прочесть;
  • itemProxy  —  только что созданный объект-администратор.

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

Возвращаемое значение функции get  —  результат чтения этого свойства. Поскольку пока мы не хотим ничего менять, мы просто возвращаем значение свойства исходного объекта. Изменяем результат, когда необходимо. Например вот так:

Вот результаты чтения его свойств:

-3

Проиллюстрирую практическое применение этого приёма. В дополнение к перехвату чтения свойств можно перехватывать их модификации:

Функция срабатывает при попытке задать значение свойству объекта:

-4

Поскольку нам нужно передать дополнительное значение при установке значения свойства, функция set принимает на один аргумент больше, чем get.

В целом Proxy перехватывает 13 операций над объектами:

  • get(item, propKey, itemProxy)—  чтение свойств, например obj.a и ojb['b']
  • set(item, propertyKey, value, itemProxy) —установка свойств: obj.a = 1
  • has(item,propKey) —  операция propKey in objProxy и возврат логического значения.
  • deleteProperty(item, propKey)—  операция delete proxy[propKey] и возврат логического значения.
  • ownKeys(item) для операцийObject.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), for...in, для возврата массива. Метод возвращает имена всех собственных свойств целевого объекта, в то время как возвращаемый результат Object.keys() включает в себя только перечисляемые свойства целевого объекта.
  • getOwnPropertyDescriptor(item, propKey): перехват операции Object.getOwnPropertyDescriptor(proxy, propKey), возврат описания свойства.
  • defineProperty(item, propKey, propDesc)для операций Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDescs), возврат логического значения.
  • preventExtensions(item): перехватчик операции операции Object.preventExtensions(proxy), возврат логического значения.
  • getPrototypeOf(item): перехватчик операции Object.getPrototypeOf(proxy), возврат объекта.
  • isExtensible(item): перехватчик операции Object.isExtensible(proxy), возврат логического значения.
  • setPrototypeOf(item, proto): перехватчик операции Object.setPrototypeOf(proxy, proto), возврат логического значения.

Если целевой объект  —  функция, можно применять две дополнительные операции перехвата:

  • apply(item, object, args)—перехват вызовов функции, таких как proxy(...args), proxy.call(object, ...args), proxy.apply(...) .
  • construct(item, args):—  перехват операции конструирования, вызванной экземпляром Proxy, например new proxy(...args).

Некоторые перехваты применяютсядовольно редко, поэтому я не буду вдаваться в подробности. Теперь посмотрим, на что прокси способен.

Отрицательные индексы массивов

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

  • arr[-1] — последний элемент массива,
  • arr[-3] — третий элемент массива с конца.

Многие считают эту функцию очень полезной, но, к сожалению, она не поддерживается в JavaScript.

-5

Но Proxy даёт возможность метапрограммирования в JavaScript. Обернём массив в объект Proxy. Когда пользователь хочет получить доступ к отрицательному индексу, мы перехватываем эту операцию методом get. Отрицательный индекс конвертируется в положительный в соответствии с заданными выше правилами, и, наконец, пользователь получает элемент. Начнём с базовой операции  —  перехвата чтения свойств массива:

Эта функция оборачивает массив. Посмотрим на её применение:

-6

Как видите, чтение свойств массива действительно было перехвачено. Имейте в виду: объекты в JavaScript могут иметь ключ только типа String или Symbol. Когда мы пишем arr[1], фактически мы обращаемся к arr[‘1’]. Ключ — это строка ‘1’, а не число 1.

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

Как обнаружить отрицательный индекс? Здесь легко ошибиться, поэтому я распишу подробнее. Прежде всего, метод get будет перехватывать доступ ко всем свойствам массива, включая доступ к его индексу и к другим свойствам массива. Операция, которая обращается к элементу в массиве, выполняется, только если имя свойства может быть преобразовано в целое число. Нам нужно перехватить эту операцию, чтобы получить доступ к элементам массива. Мы можем определить, является ли свойство массива индексом, проверив его преобразование в целое число:

Number(propKey) != NaN && Number.isInteger(Number(propKey))

Вот весь код функции:

И вывод на примере:

-7

Валидация данных

Как мы знаем, JavaScript  — слабо типизированный язык. Обычно при создании объекта объект остаётся открытым, то есть кто угодно может его изменить. Но в большинстве случаев значение свойства объекта должно соответствовать определённым условиям. Например, объект, записывающий пользовательскую информацию в поле возраст, должен быть целым числом больше нуля и меньше 150:

let person1 = {
name: 'Jon',
age: 23
}

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

person1.age = 9999
person1.age = 'hello world'

Чтобы сделать код безопаснее, можно обернуть объект в Proxy. Мы можем перехватить операцию set и проверить, соответствует ли новое значение поля age каким-то правилам:

Теперь попробуем изменить значение этого свойства и увидим, что механизм защиты работает:

-8

Ассоциированное свойство

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

Чтобы угодить пользователям из разных стран, я использую отвлечённый пример. Предположим, местоположение и почтовый индекс соотносятся следующим образом:

JavaScript Street -- 232200

Python Street
-- 234422

Goland Street
-- 231142

Вот результат выражения их отношений в коде:

Затем рассмотрим такой пример:

Нам нужно автоматически вызывать person.location='JavaScript Street' при вводе person.postcode=232200. И вот простейшее решение в лоб:

-9

Мы связали postcode и location.

Приватные свойства

Мы знаем, что приватные свойства никогда не поддерживались в JavaScript, что не позволяет управлять правами доступа к ним. Для решения этой проблемы существует соглашение сообщества JavaScript: поля, начинающиеся с символа _, рассматриваются как приватные:

var obj = {
a: 1,
_value: 22
}

Свойство _value выше рассматривается как приватное. Важно помнить, что это просто соглашение, то есть на уровне языка такого правила не существует. Теперь с помощью Proxy можно смоделировать приватные свойства. Приватные свойства обладают такими особенностями:

  • Их значение не может быть прочитано.
  • Когда пользователь пытается получить доступ к ключу объекта, приватное свойство скрыто.

Теперь изучим 13 операций перехвата прокси, упоминавшиеся выше, и увидим, что нам нужно перехватить только 3 из них:

Затем добавим в код условие: если пользователь пытается получить доступ к полю, начинающемуся с символа _, доступ запрещается. [прим. ред.  —  Примеры не совсем оптимальны, их задача  —  демонстрация, а не эффективность]:

Пример вывода:

-10

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи bitfish: Why Proxy is a Gem in JavaScript?