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


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

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

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

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

// Нормальный объект let obj = {a: 1, b:2} // Настройка obj с администратором с помощью Proxy let objProxy = new Proxy(obj, handler)

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

let obj = {a: 1, b:2} // Используем синтаксис Proxy в поиске администратора для объекта let objProxy = new Proxy(obj, { get: function(item, property, itemProxy){ console.log(`You are getting the value of '${property}' property`) return item[property] } })

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

{ get: function(item, property, itemProxy){ console.log(`You are getting the value of '${property}' property`) return item[propery] }

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

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

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

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

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

let obj = {a: 1, b:2} let objProxy = new Proxy(obj, { get: function(item, property, itemProxy){ console.log(`You are getting the value of '${property}' property`) return item[property] * 2 } })

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

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

let obj = {a: 1, b:2} let objProxy = new Proxy(obj, { set: function(item, property, value, itemProxy){ console.log(`You are setting '${value}' to '${property}' property`) item[property] = value } })

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

Поскольку нам нужно передать дополнительное значение при установке значения свойства, функция 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. Отрицательный индекс конвертируется в положительный в соответствии с заданными выше правилами, и, наконец, пользователь получает элемент. Начнём с базовой операции  —  перехвата чтения свойств массива:

function negativeArray(array) { return new Proxy(array, { get: function(item, propKey){ console.log(propKey) return item[propKey] } }) }

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

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

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

function negativeArray(array) { return new Proxy(array, { get: function(target, propKey){ if(/** propKey - отрицательный индекс **/){ // перевод отрицательного индекса в положительный } return target[propKey] }) }

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

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

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

function negativeArray(array) { return new Proxy(array, { get: function(target, propKey){ if (Number(propKey) != NaN && Number.isInteger(Number(propKey)) && Number(propKey) < 0) { propKey = String(target.length + Number(propKey)); } return target[propKey] } }) }

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

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

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

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

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

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

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

let ageValidate = { set (item, property, value) { if (property === 'age') { if (!Number.isInteger(value) || value < 0 || value > 150) { throw new TypeError('age should be an integer between 0 and 150'); } } item[property] = value } }

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

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

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

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

JavaScript Street -- 232200 Python Street -- 234422 Goland Street -- 231142

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

const location2postcode = { 'JavaScript Street': 232200, 'Python Street': 234422, 'Goland Street': 231142 } const postcode2location = { '232200': 'JavaScript Street', '234422': 'Python Street', '231142': 'Goland Street' }

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

let person = { name: 'Jon' } person.postcode = 232200

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

let postcodeValidate = { set(item, property, value) { if(property = 'location') { item.postcode = location2postcode[value] } if(property = 'postcode'){ item.location = postcode2location[value] } } }

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

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

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

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

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

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

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

function setPrivateField(obj, prefix = "_"){ return new Proxy(obj, { // Перехват операции`propKey in objProxy` has: (obj, prop) => {}, // Перехват `Object.keys(proxy)` ownKeys: obj => {}, //Перехват чтения свойств объекта get: (obj, prop, rec) => {}) }); }

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

function setPrivateField(obj, prefix = "_"){ return new Proxy(obj, { has: (obj, prop) => { if(typeof prop === "string" && prop.startsWith(prefix)){ return false } return prop in obj }, ownKeys: obj => { return Reflect.ownKeys(obj).filter( prop => typeof prop !== "string" || !prop.startsWith(prefix) ) }, get: (obj, prop) => { if(typeof prop === "string" && prop.startsWith(prefix)){ return undefined } return obj[prop] } }); }

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


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


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.