Источник: Nuances of Programming
Что такое прокси? Что именно он делает? Перед тем, как разобраться, посмотрим на пример из реальной жизни. У каждого из нас есть множество ежедневных задач — просматривание электронной почты, получение экспресс-доставки и так далее. Временами мы чувствуем себя немного взволнованными: слишком много времени уходит, чтобы разобраться в огромном потоке спама, а в посылке может оказаться бомба, подброшенная террористами.
И вот здесь вам нужен надёжный администратор. Вы хотите, чтобы он проверял почту и удалял спам до того, как вы приступите к чтению, а при получении посылки проверял содержимое с помощью профессионального оборудования, чтобы убедиться, что внутри нет бомбы. В этом примере администратор — наш заместитель, прокси. Пока мы выполняем свои задачи, администратор выполняет дополнительные.
Теперь вернёмся к JavaScript. Мы знаем, что JS — объектно-ориентированный язык, то есть мы не можем написать код, не используя объекты. Но объекты в JavaScript всегда работают без оболочки, с ними можно делать что угодно. Это, в свою очередь, делает код небезопасным.
Объект Proxy представлен в ECMAScript2015. С его помощью можно найти локального администратора для объекта и расширить исходные функции объекта. На самом простом уровне применение Proxy выглядит так:
Пока это только заготовка — без обработчика этот код не работает корректно. Человек может поручить администратору чтение писем, получение доставки и тому подобные задачи. Администратор может читать и задавать свойства и так далее. Задачи могут быть расширены с помощью Proxy. В обработчике можно перечислить действия, для которых нужен Proxy. Например, чтобы отобразить инструкцию в консоли при получении свойства объекта, напишем такой код:
Обработчик из примера выше:
Когда мы читаем свойства объекта, выполняется функция get.
get принимает три аргумента:
- item — сам объект;
- property — имя свойства, которое нужно прочесть;
- itemProxy — только что созданный объект-администратор.
Если вы читали руководства по прокси, то, наверное, заметили, что я использую другие имена параметров. Я делаю это, чтобы упростить понимание примера.
Возвращаемое значение функции get — результат чтения этого свойства. Поскольку пока мы не хотим ничего менять, мы просто возвращаем значение свойства исходного объекта. Изменяем результат, когда необходимо. Например вот так:
Вот результаты чтения его свойств:
Проиллюстрирую практическое применение этого приёма. В дополнение к перехвату чтения свойств можно перехватывать их модификации:
Функция срабатывает при попытке задать значение свойству объекта:
Поскольку нам нужно передать дополнительное значение при установке значения свойства, функция 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.
Но Proxy даёт возможность метапрограммирования в JavaScript. Обернём массив в объект Proxy. Когда пользователь хочет получить доступ к отрицательному индексу, мы перехватываем эту операцию методом get. Отрицательный индекс конвертируется в положительный в соответствии с заданными выше правилами, и, наконец, пользователь получает элемент. Начнём с базовой операции — перехвата чтения свойств массива:
Эта функция оборачивает массив. Посмотрим на её применение:
Как видите, чтение свойств массива действительно было перехвачено. Имейте в виду: объекты в JavaScript могут иметь ключ только типа String или Symbol. Когда мы пишем arr[1], фактически мы обращаемся к arr[‘1’]. Ключ — это строка ‘1’, а не число 1.
Наша задача сделать так, чтобы когда пользователь хочет получить доступ к свойству, которое является индексом массива, и обнаруживается, что индекс отрицателен, свойство перехватывается и обрабатывается соответствующим образом. Если свойство не является индексом или индекс положительный, мы ничего не делаем. Напишем такой код:
Как обнаружить отрицательный индекс? Здесь легко ошибиться, поэтому я распишу подробнее. Прежде всего, метод get будет перехватывать доступ ко всем свойствам массива, включая доступ к его индексу и к другим свойствам массива. Операция, которая обращается к элементу в массиве, выполняется, только если имя свойства может быть преобразовано в целое число. Нам нужно перехватить эту операцию, чтобы получить доступ к элементам массива. Мы можем определить, является ли свойство массива индексом, проверив его преобразование в целое число:
Number(propKey) != NaN && Number.isInteger(Number(propKey))
Вот весь код функции:
И вывод на примере:
Валидация данных
Как мы знаем, JavaScript — слабо типизированный язык. Обычно при создании объекта объект остаётся открытым, то есть кто угодно может его изменить. Но в большинстве случаев значение свойства объекта должно соответствовать определённым условиям. Например, объект, записывающий пользовательскую информацию в поле возраст, должен быть целым числом больше нуля и меньше 150:
let person1 = {
name: 'Jon',
age: 23
}
По умолчанию, однако, JavaScript не предоставляет механизм безопасности, и это значение при желании можно изменить:
person1.age = 9999
person1.age = 'hello world'
Чтобы сделать код безопаснее, можно обернуть объект в Proxy. Мы можем перехватить операцию set и проверить, соответствует ли новое значение поля age каким-то правилам:
Теперь попробуем изменить значение этого свойства и увидим, что механизм защиты работает:
Ассоциированное свойство
Свойства объекта связаны друг с другом. Например, для объекта, хранящего пользовательскую информацию, почтовый индекс и местоположение тесно связаны. Когда определён почтовый индекс пользователя, определено и его местоположение.
Чтобы угодить пользователям из разных стран, я использую отвлечённый пример. Предположим, местоположение и почтовый индекс соотносятся следующим образом:
JavaScript Street -- 232200
Python Street -- 234422
Goland Street -- 231142
Вот результат выражения их отношений в коде:
Затем рассмотрим такой пример:
Нам нужно автоматически вызывать person.location='JavaScript Street' при вводе person.postcode=232200. И вот простейшее решение в лоб:
Мы связали postcode и location.
Приватные свойства
Мы знаем, что приватные свойства никогда не поддерживались в JavaScript, что не позволяет управлять правами доступа к ним. Для решения этой проблемы существует соглашение сообщества JavaScript: поля, начинающиеся с символа _, рассматриваются как приватные:
var obj = {
a: 1,
_value: 22
}
Свойство _value выше рассматривается как приватное. Важно помнить, что это просто соглашение, то есть на уровне языка такого правила не существует. Теперь с помощью Proxy можно смоделировать приватные свойства. Приватные свойства обладают такими особенностями:
- Их значение не может быть прочитано.
- Когда пользователь пытается получить доступ к ключу объекта, приватное свойство скрыто.
Теперь изучим 13 операций перехвата прокси, упоминавшиеся выше, и увидим, что нам нужно перехватить только 3 из них:
Затем добавим в код условие: если пользователь пытается получить доступ к полю, начинающемуся с символа _, доступ запрещается. [прим. ред. — Примеры не совсем оптимальны, их задача — демонстрация, а не эффективность]:
Пример вывода:
Читайте также:
Перевод статьи bitfish: Why Proxy is a Gem in JavaScript?