От автора: перво-наперво: официальное определение Proxy от MDN: «Объект Proxy позволяет вам создать прокси для другого объекта, который может перехватывать и переопределять основные операции для этого объекта».
Теперь, прежде чем копать глубже, давайте сначала обсудим некоторые примеры из реальной жизни, чтобы в конце мы могли лучше понять Proxy. Как у людей, у нас есть много дел в повседневной жизни, например, чтение электронных писем, получение доставки и т.д. Иногда мы можем немного беспокоиться из-за большого количества дополнительных и ненужных задач — например, существует много спамерских писем, и для того, чтобы избавиться от них, требуются некоторые усилия и время, или полученная доставка может содержать бомбы, заложенные террористами, что угрожает нашей безопасности (только вероятность).
Вот где вы хотите, чтобы кто-то защищал вас от таких угроз: верная домработница. Нам нужен кто-то, кто делает для нас что-то дополнительное, чтобы защитить то, что мы делаем.
А теперь вернемся к основам — JavaScript. Мы знаем, что можем расширить JavaScript для использования функций, предоставляемых парадигмой объектно-ориентированного программирования, таких как инкапсуляция, абстракция, классы, объекты и так далее. Можно сказать, что каждый разработчик JavaScript использует объекты, и довольно часто в объектах сохраняется некоторая информация.
Но когда мы это делаем (используем объекты), наш код становится менее безопасным. Потому что объекты JavaScript всегда работают незащищенными, и с ними можно делать что угодно.
Итак, чтобы решить эту проблему, в ECMAScript 2015 была представлена новая функция под названием Proxy. С помощью Proxy мы можем найти верного помощника по хозяйству для объекта и помочь нам улучшить исходные функции объекта.
Что такое объект JavaScript Proxy?
«Proxy JavaScript — это объект, который оборачивает другой объект (цель) и перехватывает основные операции целевого объекта».
Для человека у нас могут быть такие операции, как чтение почты, получение доставки и т.д., и экономка может сделать это за нас. Для объекта основными операциями могут быть поиск свойств, присваивание, перечисление, вызовы функций и т.д., которые также могут быть расширены с помощью прокси-объекта.
Простая иллюстрация прокси-объекта
Создание прокси-объекта
Как минимум, вы можете использовать следующий синтаксис для создания файла Proxy.
1 |
let proxy = new Proxy(target, handler); |
где:
target — это объект, который нужно обернуть
handler — это объект, содержащий методы для управления поведением target. Методы внутри объекта handler называются хуками.
Proxy создает необнаруживаемый барьер вокруг целевого объекта, который перенаправляет все операции на объект-обработчик. Если мы отправим пустой handler, прокси будет просто пустой оболочкой вокруг исходного объекта.
Простой пример прокси
Прежде всего, давайте определим новый объект с именем user.
1 2 3 4 5 |
const user = { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', } |
Теперь определите объект handler. В обработчике мы можем перечислить действия, которые хотим проксировать. Например, если мы хотим вывести оператор в консоли при получении свойства объекта, мы можем написать:
1 2 3 4 5 6 |
const handler = { get(item, property, itemProxy) { console.log(`Property ${property} has been read.`); return target[property]; } } |
Функция get может принимать три аргумента:
item : это сам объект.
property : название свойства, которое вы пытаетесь прочитать.
itemProxy : это только что созданный объект housekeeper.
Теперь создайте объект proxy, что очень просто сделать, например:
1 |
const proxyUser = new Proxy(user, handler); |
Объект proxyUser использует oбъект user для хранения данных. proxyUser может получить доступ ко всем свойствам объекта user.
Иллюстрация нашего примера кода выше
Теперь, просто зайдите в свойства firstName и lastName объекта user через объект proxyUser:
1 2 |
console.log(proxyUser.firstName); console.log(proxyUser.lastName); |
Результат будет выглядеть примерно так:
1 2 3 4 |
Property firstName has been read. John Property lastName has been read. Doe |
В приведенном выше примере возвращаемое значение функции get является результатом чтения этого свойства. Поскольку мы пока не хотим ничего менять, мы просто возвращаем значение свойства исходного объекта. При необходимости мы также можем изменить результат. Например, мы можем сделать это:
1 2 3 4 5 6 7 8 9 |
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) |
Он выведет это:
1 2 3 4 |
You are getting the value of 'a' property 2 You are getting the value of 'b' property 4 |
Ловушка прокси
Ловушка get()
Ловушка Хук get() срабатывает, когда свойство объекта target осуществляется через объект прокси. В предыдущем примере сообщение выводится, когда объект user обращается к свойству объекта proxyUser.
Ловушка set()
Помимо перехвата чтения свойств, мы также можем перехватывать изменения свойств. Как, например:
1 2 3 4 5 6 7 8 |
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 и obj[‘b’].
set (item, propKey, value, itemProxy): перехватывает операцию установки свойств объекта, например, obj.a = 1.
has (item, propKey): перехватывает операцию propKey в 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).
Теперь давайте рассмотрим некоторые варианты использования и посмотрим, как на самом деле Proxy может нам помочь.
Реализует отрицательный индекс массива
Некоторые языки программирования, например, Python, поддерживают отрицательные индексы массивов. Отрицательный индекс принимает последнюю позицию массива в качестве отправной точки и ведет отсчет вперед. Например:
arr [-1] — последний элемент массива.
arr [-4] — четвертый элемент в массиве с конца.
Это, несомненно, мощная и полезная функция. Но, к сожалению, в настоящее время JavaScript не поддерживает отрицательные индексы массива. Если вы попытаетесь это сделать, вы получите undefined, например:
Здесь Proxy может быть очень полезным, если нам действительно нужно использовать отрицательные индексы в нашем коде.
Мы можем обернуть массив как прокси — объект. Когда пользователь пытается получить доступ к отрицательному индексу, мы можем перехватить эту операцию с помощью метода прокси get. Затем отрицательный индекс преобразуется в положительный индекс в соответствии с ранее определенными правилами, и доступ завершается.
Посмотрим, как именно этого добиться с помощью прокси. Итак, наше требование: когда пользователь пытается получить доступ к свойству, которое является индексом массива, и оказывается, что это отрицательный индекс, затем перехватить и обработать его соответствующим образом. Если свойство не является индексом или если индекс положительный, мы ничего не делаем.
Прежде всего, метод прокси get будет перехватывать доступ ко всем свойствам массива, включая доступ к индексу массива и доступ к другим свойствам массива. Операция, которая обращается к элементу в массиве, выполняется, только если имя свойства может быть преобразовано в целое число. На самом деле нам нужно перехватить эту операцию, чтобы получить доступ к элементам в массиве.
Мы можем определить, является ли свойство массива индексом, проверив, можно ли его преобразовать в целое число.
1 |
Number(propKey) != NaN && Number.isInteger(Number(propKey)) |
Вот полный код:
1 2 3 4 5 6 7 8 9 10 |
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 Developer Tool.
Валидация данных
Как мы знаем, javascript — язык со слабой типизацией. Обычно, когда объект создается, он работает незащищенным. Любой может изменить его.
Но в большинстве случаев значение свойства объекта требуется для удовлетворения определенных условий. Например, объект, который записывает информацию о пользователе, должен иметь целое число больше 0 в поле возраста, обычно меньше 150.
1 2 3 4 |
let person1 = { name: 'Jon', age: 23 } |
Однако по умолчанию JavaScript не обеспечивает механизма безопасности, и вы можете изменить это значение по своему желанию.
1 2 |
person1.age = 9999 person1.age = 'hello world' |
Чтобы сделать наш код более безопасным, мы можем обернуть наш объект в прокси. Мы можем перехватить операцию установки объекта и проверить, соответствует ли новое значение поля возраста правилам. Вот как именно это можно сделать с помощью кода:
1 2 3 4 5 6 7 8 9 10 |
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 } } |
Теперь мы пытаемся изменить значение этого свойства и видим, что установленный нами механизм защиты работает.
Связанное свойство
Часто свойства объекта связаны друг с другом. Например, для объекта, который хранит информацию о пользователе, его почтовый индекс и местоположение являются двумя сильно коррелированными свойствами. Когда определяется почтовый индекс пользователя, также определяется его местоположение.
Чтобы примирить читателей из разных стран, я использую виртуальный пример. Предположим, что местоположение и почтовый индекс имеют следующие отношения:
1 2 3 |
JavaScript Street -- 232200 Python Street -- 234422 Golang Street -- 231142 |
Это результат выражения их отношений в коде.
1 2 3 4 5 6 7 8 9 10 11 |
const location2postcode = { 'JavaScript Street': 232200, 'Python Street': 234422, 'Golang Street': 231142 } const postcode2location = { '232200': 'JavaScript Street', '234422': 'Python Street', '231142': 'Golang Street' } |
Теперь посмотрите на пример:
1 2 3 4 5 |
let person = { name: 'Jon' } person.postcode = 232200 |
Мы хотим иметь сделать так, чтобы person.location=’JavaScript Street’ автоматически срабатывало при установке person.postcode=232200. Вот решение:
1 2 3 4 5 6 7 8 9 10 11 |
let postcodeValidate = { set(item, property, value) { if(property === 'location') { item.postcode = location2postcode[value] } if(property === 'postcode'){ item.location = postcode2location[value] } } } |
Итак, мы связали postcode и location вместе.
Закрытые свойства
Мы знаем, что закрытые свойства никогда не поддерживались в JavaScript. Это делает невозможным разумное управление правами доступа при написании кода. Чтобы решить эту проблему, сообщество JavaScript считает, что поля, начинающиеся с символа _, считаются закрытыми свойствами.
1 2 3 4 |
var obj = { a: 1, _value: 22 } |
Свойство _value считается закрытым. Однако важно отметить, что это всего лишь соглашение, и на уровне языка такого правила нет.
Теперь, когда у нас есть прокси, мы можем смоделировать функцию закрытого свойства. По сравнению с обычным свойством, закрытое свойство имеет следующие особенности:
Значение этого свойства не читается;
Когда пользователь пытается получить доступ к ключу объекта, свойство не видно.
Затем мы можем исследовать 13 операций перехвата прокси, которые мы упоминали ранее, и увидеть, что есть 3 операции, которые необходимо перехватить.
1 2 3 4 5 6 7 8 |
function setPrivateField(obj, prefix = "_"){ return new Proxy(obj, { // Intercept the operation of `propKey in objProxy` has: (obj, prop) => {}, // Intercept the operations such as `Object.keys(proxy)` ownKeys: obj => {}, //Intercepts the reading operation of object properties get: (obj, prop, rec) => {}) }); } |
Затем мы добавляем в шаблон соответствующее условие: если обнаруживается, что пользователь пытается получить доступ к полю, которое начинается с _, доступ запрещается.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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, каковы некоторые из его вариантов использования. Теперь мы знаем, как использовать прокси для слежки за объектами. Теперь вы должны иметь возможность добавлять к ним поведения, используя методы ловушки в объекте-обработчике. Я надеюсь, что теперь вы достаточно воодушевлены, чтобы изучить возможности ProxyJavaScript. Спасибо за прочтение. Надеюсь, эта статья была для вас полезной.
Автор: Gourav Kajal
Источник: medium.com
Редакция: Команда webformyself.