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

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

Теперь вернёмся к 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]
    }
  });
}

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

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

Читайте нас в Telegram, VK и Яндекс.Дзен


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

Предыдущая статьяПять действительно крутых пакетов Python
Следующая статьяСобеседование в Facebook. Ценный опыт и открытия