Что такое прокси? Что именно он делает? Перед тем, как разобраться, посмотрим на пример из реальной жизни. У каждого из нас есть множество ежедневных задач — просматривание электронной почты, получение экспресс-доставки и так далее. Временами мы чувствуем себя немного взволнованными: слишком много времени уходит, чтобы разобраться в огромном потоке спама, а в посылке может оказаться бомба, подброшенная террористами.
И вот здесь вам нужен надёжный администратор. Вы хотите, чтобы он проверял почту и удалял спам до того, как вы приступите к чтению, а при получении посылки проверял содержимое с помощью профессионального оборудования, чтобы убедиться, что внутри нет бомбы. В этом примере администратор — наш заместитель, прокси. Пока мы выполняем свои задачи, администратор выполняет дополнительные.
Теперь вернёмся к 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]
}
});
}
Пример вывода:
Читайте также:
- Создайте собственный AdBlocker за 10 минут
- Rust для разработчиков JS
- Почему нельзя прерывать цикл forEach в JavaScript
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи bitfish: Why Proxy is a Gem in JavaScript?