Все, что вам нужно знать о Proxy в JavaScript
Очередная статья о Proxy в JavaScript, в которой содержаться несколько примеров ее применения.
Перевод статьи: Gourav Kajal — Everything You Should Know About JavaScript Proxy
Определение Proxy
:
“Прокси в JavaScript — это объект, который обертывает другой объект с целью перехвата основных операции над объектом.” — Javascript Tutorial
Создание proxy
Для создания прокси объекта можно использовать следующий синтаксис.
let proxy = new Proxy(target, handler);
где
target
это объект, который нужно обернутьhandler
это объект, содержащий методы для управления поведением target. Методы внутри объектаhandler
называются ловушками (traps).
Proxy
создает необнаруживаемый барьер вокруг целевого объекта, который перенаправляет все операции на объект-обработчик. Если мы отправим пустой handler, прокси будет просто пустой оболочкой вокруг исходного объекта.
Простой пример proxy
Прежде всего, давайте определим новый объект с именем user.
const user = { firstName: ‘John’, lastName: ‘Doe’, email: ‘john.doe@example.com’, }
Теперь определим объект обработчик (handler
):
В обработчике мы можем перечислить действия, которые хотим проксировать. Например, если мы хотим распечатать оператор в консоли при получении свойства объекта, мы можем написать так:
const handler = { get(item, property, itemProxy) { console.log(`Property ${property} has been read.`); return target[property]; } }
Функция get может принимать три аргумента:
item
: это сам объект.property
: название свойства, которое вы пытаетесь прочитать.itemProxy
: это только что созданный объект housekeeper.
Теперь создайте proxy
объект, что очень просто сделать, например:
const proxyUser = new Proxy(user, handler);
Объект proxyUser использует объект пользователя для хранения данных. ProxyUser может получить доступ ко всем свойствам объекта пользователя.
Теперь давайте просто обратимся к свойствам firstName и lastName объекта пользователя через объект 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 Traps (Ловушки)
get()
trap
Ловушка get() срабатывает, когда к свойству target
объекта осуществляется доступ через прокси-объект.
В предыдущем примере сообщение распечатывается, когда объект proxyUser обращается к свойству объекта user
.
set()
trap
Помимо перехвата чтения свойств, мы также можем перехватывать изменения свойств. Как в этом примере:
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 и возвращает boolean.
- deleteProperty(item, propKey): Перехватывает операцию удаления proxy[propKey] и возвращает boolean.
- 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), возвращает boolean.
- preventExtensions(item): Перехватывает операцию Object.preventExtensions(proxy), возвращает boolean.
- getPrototypeOf(item): Перехватывает операцию Object.getPrototypeOf(proxy) возвращает объект.
- isExtensible(item): Перехватывает операцию Object.isExtensible(proxy),возвращает boolean
- setPrototypeOf(item, proto): Перехватывает операцию Object.setPrototypeOf(proxy, proto),возвращает boolean
Если объект target является функцией, необходимо перехватить две дополнительные операции.
- apply(item, object, args): Перехватывает операции вызова функций, такие как
proxy(...args)
,proxy.call(object, ...args)
,proxy.apply(...)
. - construct(item, args): Перехватывает операцию, вызванную экземпляром Proxy в качестве конструктора, например
new proxy(...args)
.
Теперь давайте рассмотрим некоторые варианты использования и посмотрим, как можно использовать Proxy. Ниже приведены примеры использования bitfish в его статье.
Реализуем отрицательный индекс массива
Некоторые языки программирования, например 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.
Проверка данных
Как мы знаем, javascript — язык со слабой типизацией. Обычно, когда объект создается, он используется как есть и любой может изменить его.
Но в большинстве случаев значение свойства объекта требует соответствия определенным условиям. Например, объект, который записывает информацию о пользователе, должен иметь целое число больше 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.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] } } }
Итак, мы связали почтовый индекс и местоположение вместе.
Приватные свойства
Мы знаем, что приватные свойства раньше не поддерживались в JavaScript. Это лишало нас возможности разумно управлять правами доступа при написании кода.
Для решения этой проблемы в сообществе JavaScript принято, что поля, начинающиеся с символа _, считаются закрытыми свойствами.
var obj = { a: 1, _value: 22 }
Указанное выше свойство _value считается приватным. Однако важно отметить, что это всего лишь соглашение, и на уровне языка такого правила нет.
Теперь, когда у нас есть прокси, мы можем смоделировать приватное свойство.
По сравнению с обычным свойством, приватные имеет следующие особенности:
- Значение этого свойства не может быть прочитано снаружи объекта
- Когда пользователь пытается получить доступ к свойству через перечисление всех свойств объекта, этого свойство ну будет видно.
Для этого мы можем изучить 13 операций перехвата прокси, о которых упоминалось ранее, и увидеть, что есть 3 операции, которые необходимо перехватить.
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) => {}) }); }
Затем мы добавляем в шаблон соответствующий код: если обнаружено, что пользователь пытается получить доступ к свойству, которое начинается с _, доступ запрещается.
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] } }); }
Вот окончательный пример использования:
Заключение
В статье мы рассмотрели несколько способов использования прокси в JavaScript. Надеюсь эти примеры помогут вам лучше понять как использовать прокси в своих приложениях на JavaScript.