
Также откройте в новой вкладке наш предыдущий пост: JavaScript-прокси: и красиво, и полезно
Что такое прокси?
Прокси, в широком смысле, это некая доверенная сущность, выступающая от имени другой сущности. Прокси — это заменитель реального объекта, у которого есть право выступать от имени и в интересах этого объекта. «Объектом» в данном случае может быть практически всё что угодно. Если же ограничиться рассмотрением прокси в применении к JavaScript, то можно сказать, что это — особые объекты, которые позволяют перехватывать и изменять действия, выполняемые над другими объектами. В частности, речь идёт о вызове функций, об операциях присваивания, о работе со свойствами, о создании новых объектов, и так далее. Эту технологию используют для блокирования прямого доступа к целевому объекту или целевой функции и организации взаимодействия с объектом или функцией через прокси-объект.
Прежде чем мы продолжим разговор о прокси-объектах и перейдём к примерам работы с ними, рассмотрим три важных термина, имеющих к ним непосредственное отношение. Подробнее об этом можно почитать здесь.
- Обработчик (Handler) — объект-заменитель, содержащий перехватчики.
- Перехватчики (Traps) — методы, которые предоставляют доступ к свойствам.
- Целевой объект (Target) — объект, который виртуализируется прокси.
Синтаксис
Вот как выглядит объявление простого прокси-объекта, которому передаётся целевой объект и обработчик.
let proxy = new Proxy(target, handler);
Проверка поддержки прокси-объектов браузером
Начнём с проверки поддержки прокси-объектов JavaScript браузером.
let proxyTest = new Proxy({}, {})
if (proxyTest instanceof Object) {
document.write("Proxy supported!");
}
Стандартное поведение объектов
Здесь мы объявим объект, а затем попробуем обратиться к несуществующему свойству этого объекта.
let obj = {
c: "car",
b: "bike"
};
document.write(obj.b, "
"); //Результат -> "bike"
document.write(obj.c, "
"); //Результат -> "car"
document.write(obj.l); //Результат -> "undefined"
Использование прокси для объекта
В следующем примере мы используем обработчик с перехватчиком
get
. Обработчик передаст целевой объект и запрошенный ключ перехватчику.let handler = {
get: function(target, name) {
return name in target ? target[name] : "Key does not exist";
}
}
let obj = {
c: "car",
b: "bike"
};
let proxyObj = new Proxy(obj, handler);
document.write(proxyObj.b, "
"); //Результат -> "bike"
document.write(proxyObj.c, "
"); //Результат -> "car"
document.write(proxyObj.l); //Результат -> "Key does not exist"
Перехватчик get
Перехватчик
get
выполняется при попытке доступа к свойству объекта с использованием прокси. Метод get
принимает параметр target
(объект, с которым нужно работать через прокси) и property
(свойство, к которому мы пытаемся получить доступ).?Синтаксис
var p = new Proxy(target, {
get: function(target, property, receiver) {
}
});
target
: целевой объект.property
: имя свойства, с которым нужно работать.receiver
: прокси-объект или объект, унаследованный от прокси-объекта
?Пример
В следующем примере мы попытаемся воздействовать на значение с помощью перехватчика
get
до вывода его на экран.let handler = {
get: function(target, name) {
return name in target ? target[name]*10 : "Key does not exist";
}
}
let obj = {
a: 1,
b: 2
};
let proxyObj = new Proxy(obj, handler);
document.write(proxyObj.a, "
"); //Результат -> 10
document.write(proxyObj.b, "
"); //Результат -> 20
document.write(proxyObj.c); //Результат -> "Key does not exist"
Перехватчик set
Перехватчик
set
выполняется при попытке установки свойства объекта с использованием прокси. Метод set
принимает параметр target
(объект, доступ к которому мы собираемся получить), property
(свойство, которое мы собираемся устанавливать), и value
(значение свойства, которое мы собираемся установить).?Синтаксис
var p = new Proxy(target, {
set: function(target, property, value, receiver) {
}
});
target
: целевой объект.property
: имя свойства, которое нужно устанавливать.value
: новое значение интересующего нас свойства.receiver
: объект, которому изначально была направлена операция присваивания. Обычно — это сам прокси. Однако обработчикset
может быть вызван не напрямую, а через цепочку прототипов или каким-то другим способом.
?Пример
В следующем примере мы добавим к объекту некоторые свойства, назначим им значения, при этом сделаем это до объявления прокси-объекта (в коде он носит имя
proxyObj
).Проанализировав этот пример, можно заметить, что если после объявления
proxyObj
попытаться задать новое свойство объекта, будет вызван перехватчик и в объекте будет сохранено значение свойства, изменённое им.let handler = {
set: function(target, name, value) {
target[name] = value*10;
}
}
let obj = {
a: 1,
b: 2
};
let proxyObj = new Proxy(obj, handler);
proxyObj.c = 3;
document.write(proxyObj.a, "
"); //Результат -> 1
document.write(proxyObj.b, "
"); //Результат -> 2
document.write(proxyObj.c); //Результат -> 30
Перехватчик has
Перехватчик
has
вызывается при выполнении оператора in
. Метод has
принимает параметры target
(целевой объект) и property
(свойство, доступ к которому нужно контролировать с помощью прокси-объекта).?Синтаксис
var p = new Proxy(target, {
has: function(target, property) {
}
});
target
: целевой объект.property
: имя свойства, существование которого будет проверяться.
?Пример
В следующем примере мы проверяем, входит ли в ключ подстрока
ar
. Для начала проверим, существует ли ключ, и, если это так, проверим, содержит ли он интересующую нас подстроку. Если будут выполнены оба условия, мы вернём логическое значение true
, в противном случае — вернём false
.const handler = {
has: function(target, key) {
if (key in target && key.includes("ar")) {
return true;
}
return false;
}
};
const user = {
car: 'Bentley',
bar: 4,
hotel: "no",
};
const proxy = new Proxy(user, handler);
document.write('car' in proxy, "
"); // Результат -> true
document.write('car' in user, "
"); // Результат -> true
document.write('hotel' in proxy, "
"); // Результат -> false
document.write('hotel' in user, "
"); // Результат -> true
document.write('spacebar' in proxy, "
"); // Результат -> false
document.write('spacebar' in user); // Результат -> false
Перехватчик apply
Перехватчик
apply
позволяет вызывать прокси с параметрами. Он позволяет переопределять функции. Метод apply
принимает параметры target
(целевой объект или целевая функция), thisArg
(аргумент this
для использования при вызове) и argumentsList
(список всех аргументов в виде массива).?Синтаксис
var p = new Proxy(target, {
apply: function(target, thisArg, argumentsList) {
}
});
target
: целевой объект.thisArg
: аргументthis
для вызова.argumentsList
: список аргументов для вызова.
?Пример
В следующем примере мы, с помощью прокси, переопределим функцию, которая умножает два числа. Прокси меняет поведение функции, прибавляя 1 к результату умножения.
function multiply(a, b) {
return a * b;
}
const handler = {
apply: function(target, thisArg, argumentsList) {
return target(argumentsList[0], argumentsList[1]) + 1;
}
};
var proxy = new Proxy(multiply, handler);
document.write(multiply(2, 5), "
"); // Результат -> 10
document.write(proxy(2, 5)); // Результат -> 11
Перехватчик construct
Перехватчик
construct
выполняется при вызове оператора new
. Для того чтобы этот перехватчик мог нормально функционировать, нужно, чтобы целевой объект, для работы с которым его планируется вызывать, можно было бы создать командой вида new Target()
.?Синтаксис
var p = new Proxy(target, {
construct: function(target, argumentsList, newTarget) {
}
});
target
: целевой объект.argumentList
: список аргументов для конструктора.newTarget
: исходный конструктор
?Пример
В следующем примере мы передадим через прокси новое значение, к которому добавлен символ валюты, функции-конструктору.
function formatCurrency(format) {
this.format = format;
}
const handler = {
construct: function(target, args) {
return new target("$" + args[0]);
}
};
const proxy = new Proxy(formatCurrency, handler);
document.write(new proxy('100').format); // Результат -> $100
Перехватчик deleteProperty
Перехватчик
deleteProperty
вызывается при обращению к методу delete
. Он принимает параметры target
(целевой объект или целевая функция), и property
(свойство, команду удаления которого мы хотим обрабатывать).?Синтаксис
var p = new Proxy(target, {
deleteProperty: function(target, property) {
}
});
target:
целевой объект.property
: имя свойства, в операцию удаления которого нужно вмешаться.?Пример
Следующий пример демонстрирует вызов нужной нам функции и выполнение определённых действий при попытке удаления свойства объекта.
const cars = {
merc: 's320',
buggati: 'veyron',
};
const handler = {
deleteProperty: function(target, prop) {
if (prop in target) {
document.write(`${prop} has been removed
`); // Результат -> merc has been removed
delete target[prop];
}
}
};
document.write(cars.merc, "
"); // Результат -> "s320"
const proxy = new Proxy(cars, handler);
delete proxy.merc;
document.write(cars.merc, "
"); // Результат -> undefined
Варианты использования прокси
Вот несколько областей практического применения прокси-объектов в JavaScript.
- Безопасность. Прокси позволяют организовать проверку аргументов функций и значений свойств объектов.
- Хранение данных. Прокси-функция может вызываться при попытках модификации свойств объектов и создавать резервные копии данных.
- Статистика. Прокси-объекты позволяют собирать статистические сведения об объектах, с которыми взаимодействуют пользователи или некие программные механизмы приложений.
- Учёт контекста. Различные прокси можно использовать для различных контекстов выполнения программ. Скажем, это может быть контекст отладки или контекст продакшн-среды. В случае с отладкой прокси могут, например, представлять значения переменных и давать возможности прямого воздействия на содержимое объектов.
- Шаблон проектирования «посредник». Прокси являются посредниками, используемыми для работы с представляемыми ими объектами, позволяющими объектам взаимодействовать не напрямую, а через них. В результате, при использовании прокси, не нужно описывать механизмы непосредственного взаимодействия объектов друг с другом.
- Управление доступом. При организации доступа к свойствам объектов через прокси можно управлять происходящим, например, запрещать пользователю или группе пользователей доступ к объекту. Эти ограничения можно вводить либо на основании внутренней логики прокси-объектов, либо отдавая этим объектам явные команды, касающиеся ограничения доступа к целевым объектам.
Итоги
В одном из наших предыдущих материалов о прокси-объектах в JS, который был опубликован в конце января сего года, мы вынесли на голосование следующий вопрос: «Пользуетесь ли вы прокси-объектами в своих JS-приложениях?». Тогда оказалось, что 13% респондентов ответили «Да», 83% — «Нет», а 4% выбрали вариант «Другое (в комментариях)». Мир JavaScript развивается очень быстро. Вполне возможно, что сейчас изменилось и отношение программистов к прокси-объектам. Поэтому предлагаем всем желающим помочь нам это выяснить и принять участие в сегодняшнем опросе.

Комментарии (14)
nizkopal
22.05.2018 20:37Ну… наверно это полезно.
Не то, чтобы мне никогда не требовалось иметь подобный объект, просто раньше это можно было организовать руками, а сейчас вот, из коробки доступно.
ReklatsMasters
22.05.2018 20:51+1Использовал Proxy когда делал парсер торрентов (bencode парсер). Идея была в том, чтобы на вход принимать Buffer, а конвертирование в строку делать при запросе, т.к. это довольно дорогая операция. Получилось довольно быстро.
mayorovp
23.05.2018 05:33А можно подробнее? Я почему-то не вижу в вашем коде никакой ленивости…
ReklatsMasters
23.05.2018 08:59Там есть файл lazy.js. Все буферы при чтении хранит в avl дереве, а при доступе к свойству конвертирует в строку.
mayorovp
23.05.2018 10:24Не вижу где эта самая конвертация происходит…
ReklatsMasters
23.05.2018 14:24Смотрите как это работает. Допустим, я разобрал торрент файл и хочу получить доступ к свойству.
const torrent = decode(file); console.log(torrent.info.name)
При доступе в свойству
info
запрос уйдёт в прокси вот тут, конвертируется в буфер и будет осуществляться поиск согласно avl tree. Если полученное значение строка, число или массив — оно вернётся как есть. А если ещё один словарь, то вернётся ещё один прокси, он оборачивается в парсере. В данном случае конвертации в строку вообще не осуществляются, поэтому, возножно, не совсем корректно называть этот метод ленивым. Но вот в ловушкеownKeys
как раз осуществляется конвертация ключей в строки при запросе.mayorovp
23.05.2018 14:39Так бы сразу и говорили что вы ключи лениво конвертируете! Я-то искал значения… Да, это имеет смысл.
Хотя я все равно не понимаю каким образом построить дерево с кучей ключей оказывается быстрее чем конвертировать их все в строки и запихать в хеш-таблицу.
Кстати, вы не пробовали вместо avl-дерева использовать сокращенный бор (он же луч)?ReklatsMasters
23.05.2018 16:12Писал код давно, не сразу вспомнил как что работает. И да, я тоже удивлён, что работа с деревьями быстрее, чем работа со строками.
Я не спец по алгоритмам на самом деле. Я искал тот, который осуществляет вставку и поиск за приемлемое время. Просто из какой то книги по алгоритмам.
Кстати, когда я его писал, для 6й или 4й ноды кажется, он неоптимально работал — не инлайнится, деопты вылезали. Сейчас в последних нодах с турбофаном стало быстрее. Ещё и скорость proxy подкрутили.
Maiami
22.05.2018 21:56В качестве эксперимента для реализации ленивого доступа к данным, чтобы подход оставался универсальным. Например, сейчас есть примерно такой код:
app.use(modules.common) app.use(modules.lazyData1) app.use(modules.lazyData2)
В самом modules.lazyData1 забираются данные из базы, они нужны только на 2 страницах, в соответствующем шаблоне, где нужны эта данные:
mixin lazyData | test proxy: #{await lazyData1.proxyObj.c}
Правда pug не поддерживает await (или я не нашла как), пришлось использовать кастомный pug
В целом компактно, универсально и удобно добавлять новые модули/плагины. Скорость не замеряла до и после
inoyakaigor
Иногда для отладки использовал. Жду когда mobx переедет с геттеров-сеттеров на прокси (обещали в пятой версии)
mayorovp
А зачем? Какую проблему это решит?
Единственное применение которое я вижу — это обращаться «через точку» к элементам словаря вместо вызовов get/set.
dagen
Mobx почти не использовал, но подозреваю, что ещё исчезнет потребность использовать `action` в декораторах к изменяющим состояние методам.
mayorovp
Но каким образом? Action как бы и сейчас можно не писать, просто операций пересчета модели и рендера будет существено больше.
inoyakaigor
В текущем Mobx все Observable массивы суть объекты и код
возвращает false.
С прокси будет true