Первым делом ознакомимся с официальным определением Proxy
на сайте веб-документации MDN, которое гласит:
“Объект Proxy
позволяет создавать прокси для другого объекта, обладая способностью перехватывать и переопределять основные операции для данного объекта”.
Прежде чем приступить к подробному разбору темы, для лучшего понимания сути Proxy
обратимся к аналогиям из жизни. В одной из статей мне встретилось следующее объяснение:
Каждый день мы занимаемся разными делами: тут e-mail надо прочитать, там посылку получить и т. д. Периодически приходится отвлекаться на решение лишних и ненужных задач, например тратить время и силы на удаление спам-рассылки. Что касается полученной посылки, то в ней вполне может находиться бомба, заложенная террористами и угрожающая безопасности людей (все возможно).
Именно в таких ситуациях нам нужен верный помощник, который оградит от подобных угроз и возьмет на себя часть обязанностей, встав на защиту нашего дела.
Вернемся к нашей главной теме — JavaScript. Как известно, его можно расширить для применения функциональностей, предоставляемых парадигмой ООП, такие как инкапсуляция, абстракция, классы, объекты и т. д. Каждый разработчик JS использует объекты, в которых обычно сохраняется информация.
Но в результате подобных действий снижается степень защищенности кода, поскольку объекты в JS всегда выполняются “оголенными”, оказываясь доступными для любых внешних воздействий.
Для решения этой проблемы в ECMAScript 2015, широко известного как ES6, была представлена новая функциональность — Proxy, с которой мы обретаем верного помощника для объекта и средство для расширения его исходных функций.
Понятие прокси-объекта JavaScript
Прокси в JavaScript — это объект, который создает обертку для другого объекта (целевого) и перехватывает основные операции с ним.
Если вернуться к нашей аналогии, то выполнение каких-то действий, например чтение e-mail, получение посылки и т. д., может взять на себя помощник. В отношении объекта такими действиями являются поиск свойств, присваивание, перечисление, вызов функции и т. д., которые способен расширить прокси-объект.
Создание прокси-объекта
Самый простой синтаксис для создания Proxy
выглядит следующим образом:
let proxy = new Proxy(target, handler);
где:
target
— целевой объект для обертывания;handler
— объект-обработчик, содержащий методы, называемые ловушками, для управления поведениемtarget
.
Proxy
создает скрытый заслон вокруг целевого объекта, перенаправляющий все операции к обработчику. Если мы передаем пустой handler
, то прокси-объект оказывается лишь пустой оберткой вокруг исходного объекта.
Простой пример прокси-объекта
Для начала определим новый объект user
.
const user = {
firstName: ‘John’,
lastName: ‘Doe’,
email: ‘[email protected]’,
}
Затем то же самое сделаем для объекта handler
:
В обработчике перечисляются действия, выполнение которых мы намерены поручить прокси-объекту. Например, если в процессе получения свойства объекта требуется вывести выражение в консоль, пишем следующим образом:
const handler = {
get(item, property, itemProxy) {
console.log(`Property ${property} has been read.`);
return target[property];
}
}
Функция get
принимает 3 аргумента:
item
: сам объект;property
: имя считываемого свойства;itemProxy
: только что созданный объект-помощник.
Легко и просто создаем объект proxy
вот таким способом:
const proxyUser = new Proxy(user, handler);
Объект proxyUser
использует объект user
для хранения данных и может обращаться ко всем его свойствам.
Теперь давайте обратимся к свойствам firstName
и lastName
объекта user
через объект proxyUser
:
console.log(proxyUser.firstName);
console.log(proxyUser.lastName);
Вывод будет выглядеть так:
Property firstName has been read.
John
Property lastName has been read.
Doe
В данном примере возвращаемое значение функции get
является результатом считывания этого свойства. Поскольку мы не намерены пока ничего менять, просто возвращаем значение свойства исходного объекта.
При необходимости мы также можем изменить результат. Например, сделаем так:
let obj = {a: 1, b:2}
let handler = {
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[property] * 2
}
}
let objProxy = new Proxy(obj, handler)
console.log(objProxy.a)
console.log(objProxy.b)
На выводе получаем:
You are getting the value of 'a' property
2
You are getting the value of 'b' property
4
Ловушки Proxy
get()
Ловушка get()
запускается при обращении к свойству объекта target
через прокси-объект.
В предыдущем примере сообщение выводится, когда proxyUser
обращается к свойству user
.
set()
Перехватывать можно не только операции чтения, но также изменения свойств. Например, следующим образом:
let obj = {a: 1, b:2}
let handler = {
set: function(item, property, value, itemProxy){
console.log(`You are setting '${value}' to '${property}' property`)
item[property] = value
}
}
let objProxy = new Proxy(obj, handler)
При попытке обновить значение свойства — увидим такой вывод:
Поскольку нам нужно передать дополнительное значение при установке значения свойства, функция set
в отличие от функции get
принимает еще один аргумент .
Помимо операций чтения и изменения свойств, прокси-объект способен перехватывать в целом 13 операций/ловушек:
get(item, propKey, itemProxy)
перехватывает операцию чтения свойств объектов, таких какobj.a
иojb['b']
.set(item, propKey, 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)
и возвращает значение логического типа.
Если целевой объект является функцией, существуют 2 дополнительные операции для перехвата.
apply(item, object, args)
перехватывает операции вызова функции, такие какproxy(...args)
,proxy.call(object, ...args)
,proxy.apply(...)
.construct(item, args)
перехватывает операцию, запускаемую экземпляром прокси-объекта в качестве конструктора, а именноnew proxy(...args)
.
Теперь разберем некоторые случаи использования и посмотрим, в чем состоит реальная польза Proxy
.
Реализация отрицательного индекса массива
Ряд языков программирования, например Python, поддерживают отрицательные индексы массивов.
Отрицательный индекс подразумевает обратный отсчет, начиная с последней позиции массива. Например:
- arr[-1] — последний элемент массива;
- arr[-4] — четвертый элемент массива с конца.
Эта функциональная возможность, несомненно, полезна и эффективна. К сожалению, на данный момент отрицательные индексы массивов в JavaScript не поддерживаются.
При попытке их применить вы получите undefined, как показано в следующем примере:
Если же нам действительно необходимы отрицательные индексы в коде, то на помощь приходит прокси-объект.
Массив можно обернуть как прокси-объект. При попытке пользователя обратиться к отрицательному индексу, мы перехватываем эту операцию посредством прокси-метода get. Затем в соответствии с ранее описанными правилами отрицательный индекс преобразуется в положительный, и доступ осуществляется.
Теперь рассмотрим, как именно этого добиться с помощью прокси-объекта.
Итак, у нас есть пользователь, стремящийся получить доступ к свойству, которое является индексом массива, при чем отрицательным. В этом случае перехватываем его и поступаем с ним соответствующим образом. Если же свойство не является индексом, или он положительный, то не предпринимаем никаких действий.
Прежде всего, прокси-метод 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]
}
})
}
Рассмотрим пример в Chrome DevTools:
Проверка данных Data Validation
Как известно, JS — слабо типизированный язык. Как правило, после своего создания объект выполняется “оголенным”, в связи с чем любой волен его изменить.
Но в большинстве случаев значение свойства объекта должно удовлетворять определенным условиям. Например, объект, записывающий информацию пользователя, в поле age должен иметь целое число больше 0 и, как правило, меньше 150.
let person1 = {
name: 'Jon',
age: 23
}
Однако по умолчанию JavaScript не предоставляет механизм защиты, в связи с чем вы можете менять это значение по своему усмотрению.
person1.age = 9999
person1.age = 'hello world'
Чтобы обезопасить код, можно обернуть объект с помощью прокси. С этой целью перехватим операцию 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
Golang Street -- 231142
В коде эта взаимосвязь выражается так:
const location2postcode = {
'JavaScript Street': 232200,
'Python Street': 234422,
'Golang Street': 231142
}
const postcode2location = {
'232200': 'JavaScript Street',
'234422': 'Python Street',
'231142': 'Golang Street'
}
Еще один пример:
let person = {
name: 'Jon'
}
person.postcode = 232200
Нам нужно, чтобы установка person.postcode=232200
автоматически запускала person.location='JavaScript Street'
.
Решение:
let postcodeValidate = {
set(item, property, value) {
if(property === 'location') {
item.postcode = location2postcode[value]
}
if(property === 'postcode'){
item.location = postcode2location[value]
}
}
}
Таким образом мы связали postcode
и location
.
Закрытое свойство
Ни для кого не секрет, что закрытые свойства никогда не поддерживались в JS, вследствие чего у нас отсутствует возможность грамотно управлять правами доступа при написании кода.
Для решения этой проблемы представители JS-сообщества условились считать поля, начинающиеся со знака _
, закрытыми свойствами.
var obj = {
a: 1,
_value: 22
}
Вышепредставленное свойство _value
считается закрытым. Однако важно помнить, что речь идет лишь о соглашении, а не о закрепленном в языке правиле.
Располагая прокси-объектом, мы можем сымитировать закрытое свойство.
По сравнению с обычными свойствами закрытые обладают следующими особенностями:
- Значение свойства нельзя прочитать.
- Если пользователь попытается получить доступ к ключу объекта, это свойство окажется скрытым.
Рассмотрим 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]
}
});
}
Перед вами окончательный вариант кода для изучаемого примера:
Заключение
Итак, вы узнали не только, что из себя представляет Proxy
в JavaScript и в каких случаях он используется, но и как с его помощью отслеживать объекты. Теперь вы сможете добавлять к ним поведение посредством методов-ловушек в обработчике. Хочется верить, что в вас пробудился интерес к дальнейшему изучению связанных с Proxy
возможностей в JS.
Благодарю за внимание! Надеюсь, материал статьи был вам полезен.
Читайте также:
- 3 совета, как стать мастером Йода по JavaScript
- Что такое Hoisting в JavaScript
- 4 секрета читаемого и производительного кода JavaScript
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Gourav Kajal: Everything You Should Know About JavaScript Proxy